diff --git a/.cargo/config.toml b/.cargo/config.toml index 8b2750adb5..c90a3806fb 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -28,8 +28,8 @@ test_details = "test --target aarch64-apple-darwin" # (native) are simply not listed. build-fastly = "build -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-asset-cache-51degrees -p trusted-server-analytics-tinybird -p trusted-server-shared-aws -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1" check-fastly = "check -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-asset-cache-51degrees -p trusted-server-analytics-tinybird -p trusted-server-shared-aws -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1" -clippy-fastly = "clippy -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-asset-cache-51degrees -p trusted-server-analytics-tinybird -p trusted-server-shared-aws -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb -p trusted-server-js -p trusted-server-openrtb --all-targets --all-features --target wasm32-wasip1 -- -D warnings" -test-fastly = "test -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-asset-cache-51degrees -p trusted-server-analytics-tinybird -p trusted-server-shared-aws -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1" +clippy-fastly = "clippy -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-provenance-encypher -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-asset-cache-51degrees -p trusted-server-analytics-tinybird -p trusted-server-shared-aws -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb -p trusted-server-js -p trusted-server-openrtb --all-targets --all-features --target wasm32-wasip1 -- -D warnings" +test-fastly = "test -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-provenance-encypher -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-asset-cache-51degrees -p trusted-server-analytics-tinybird -p trusted-server-shared-aws -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1" # Feature-on counterpart of `test-fastly`. `test-fastly` does NOT pass # --all-features, so without this the reusable-sandbox code is linted by # `clippy-fastly` but its tests never execute. @@ -40,8 +40,12 @@ test-fastly-reuse = "test -p trusted-server-adapter-fastly --target wasm32-wasip # run with the Axum adapter in CI. build-axum = "build -p trusted-server-adapter-axum" check-axum = "check -p trusted-server-adapter-axum" -clippy-axum = "clippy -p trusted-server-adapter-axum -p trusted-server-permission-signal-gpc -p trusted-server-permission-signal-gpp -p trusted-server-permission-signal-mtm -p trusted-server-permission-signal-tcf -p trusted-server-permission-signal-us-privacy -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-analytics-tinybird -p trusted-server-loading-cache-51degrees -p trusted-server-asset-cache-51degrees -p trusted-server-shared-aws -p trusted-server-shared-51degrees -p trusted-server-middleware-51degrees -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb --all-targets --all-features -- -D warnings" -test-axum = "test -p trusted-server-adapter-axum -p trusted-server-permission-signal-gpc -p trusted-server-permission-signal-gpp -p trusted-server-permission-signal-mtm -p trusted-server-permission-signal-tcf -p trusted-server-permission-signal-us-privacy -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-analytics-tinybird -p trusted-server-loading-cache-51degrees -p trusted-server-asset-cache-51degrees -p trusted-server-shared-aws -p trusted-server-shared-51degrees -p trusted-server-middleware-51degrees -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb" +# The native preparation worker the Axum adapter launches, kept out of +# every wasm alias. +test-preparation-worker = "test -p trusted-server-preparation-worker" +clippy-preparation-worker = "clippy -p trusted-server-preparation-worker --all-targets -- -D warnings" +clippy-axum = "clippy -p trusted-server-adapter-axum -p trusted-server-preparation-worker -p trusted-server-provenance-encypher -p trusted-server-permission-signal-gpc -p trusted-server-permission-signal-gpp -p trusted-server-permission-signal-mtm -p trusted-server-permission-signal-tcf -p trusted-server-permission-signal-us-privacy -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-analytics-tinybird -p trusted-server-loading-cache-51degrees -p trusted-server-asset-cache-51degrees -p trusted-server-shared-aws -p trusted-server-shared-51degrees -p trusted-server-middleware-51degrees -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb --all-targets --all-features -- -D warnings" +test-axum = "test -p trusted-server-adapter-axum -p trusted-server-preparation-worker -p trusted-server-provenance-encypher -p trusted-server-permission-signal-gpc -p trusted-server-permission-signal-gpp -p trusted-server-permission-signal-mtm -p trusted-server-permission-signal-tcf -p trusted-server-permission-signal-us-privacy -p trusted-server-cmp-osano -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-iabtl -p trusted-server-headers-geo -p trusted-server-analytics-tinybird -p trusted-server-loading-cache-51degrees -p trusted-server-asset-cache-51degrees -p trusted-server-shared-aws -p trusted-server-shared-51degrees -p trusted-server-middleware-51degrees -p trusted-server-tag-google-tag -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-iabtl -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb" # --- Cloudflare adapter (native host + wasm32-unknown-unknown) --- # Build/check target the WASM runtime (requires the `cloudflare` feature); diff --git a/CHANGELOG.md b/CHANGELOG.md index afab30b0d1..7f3f3a96ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Prepared Encypher text requests now require exact-request API cache isolation for signing, recovery and publication, so changing only a title or page URL cannot reuse an older text-and-rights-only result. The text identity hash domain is versioned to exclude old prepared records; media identities and synchronous signing remain unchanged. Missing or unknown text cache scopes are refused without a legacy fallback. +- Preparation diagnostics retain one extraction-refusal outcome per admitted page scope and reason. Unrelated HTML changes, such as comments or titles, no longer accumulate permanent duplicate outcomes or consume additional diagnostic quota. +- Preparation inventory capacity now follows the configured scan rate and whole-minute interval, up to 1,024 targets. Dynamic registration and restart enforce the same bound as configured inventory. A retained store above that capacity refuses startup without deleting records. Intervals below 60 seconds are refused because housekeeping runs once a minute; the default settings still admit 1,024 targets. +- Async preparation with a selected asset-cache module requires snapshots through `https://`, so preparation addresses the reader's held-copy cache. The equivalent trailing slash and `:443` forms are normalized; other origins refuse configuration and startup. Cache-free local development and synchronous mode are unchanged. Reader text lookup computes its shared identity without constructing an owned signing request. +- Encypher can sign bounded media with source ingredients and executed transformation actions. The opt-in large-media lane hashes a version-pinned origin locally and uses prepared signing without uploading or buffering the complete asset. It retains existing embedded, XMP-referenced and HTTP-linked provenance rather than publishing a competing manifest. Enable a bounded `max_large_media_bytes` only after the matching signed probe and backend release are available and the adapter can enforce the whole-operation deadline; the default is `0`. Fastly has no total timer and keeps this lane closed. +- Encypher's `max_media_sign_calls_per_document` limits uncached embedded-media signing calls per HTML or CSS document. It defaults to 8 and accepts 1-128. Cache hits do not consume calls; exceeding the limit leaves the remaining media unchanged without negatively caching it. Raising the limit does not extend the document's time or output-size bounds. +- Large-media namespace resolution, cache lookup and new hash/sign work share one remaining-time budget. An already exhausted budget starts no hashing. Malformed `article_selector` overrides now fail settings admission instead of silently choosing the normal article boundary; empty and valid-but-absent overrides keep that fallback. +- A declined large-media sign returns one fresh, private/no-store origin response with its own validators, redirects, authentication challenges and retry delay. Publisher redirect targets use the existing serving-host mapping; the fallback does not retry or follow redirects. +- With a media signer installed, an Image Optimizer route asks Fastly Image Optimizer to keep its input's C2PA manifest and add its own record of the transformations it performed (`metadata=c2pa`). An optimized answer that carries a C2PA manifest store is signed as opened, with Image Optimizer's manifest as its parent and `Vary: Accept`; one without stays unsigned as before. A route that sets `[proxy.asset_routes.image_optimizer] signed_source = true` sends Image Optimizer to its own signed original on the serving host, making eligible unsigned originals available as signed inputs at the cost of a second request per image, and answers once from the origin without Image Optimizer when Image Optimizer fails. Deployed Image Optimizer behavior has not been verified. +- Media signing leaves the parts of a fragmented ISO BMFF stream alone, so a live or DASH/HLS stream part within the signer's bounded limit, whose provenance its init segment binds, reaches the reader byte for byte with the origin's own headers and no competing manifest, whatever its `Content-Type` or `Cache-Control`. Core reads the bytes it already captured: a media segment starts with `styp` or has a `moof` before its first `mdat` and has no `moov`, and an init segment starts with `ftyp` and has a `moov` holding an `mvex` and no `mdat`. A progressive MP4 is signed as before. Encypher also never claims `video/iso.segment`. +- An asset route that sets `rehost_page_references = true` serves a third party's public files from the publisher's host, making eligible images, fonts and embedded stylesheet media available for signing. Pages served through the publisher pipeline name the route's address for media references (`src`, `srcset` and `imagesrcset` candidates, `video` `poster`, media preloads) and, where a `[[serve]]` entry for `text/css` covers the path, stylesheet references, but only where request dispatch reaches that route and the route fetches exactly the address the page wrote. An element with `integrity` keeps its address. The route sends no `Referer` and asks for an identity-coded body. A moved stylesheet has each address it names resolved against its upstream address and written as a route address or the absolute upstream address, then runs through its `[[serve]]` entry, without the origin's validators. A stylesheet that cannot be moved, for a status other than `200`, a coded body, a `private` or `no-transform` answer, a charset other than UTF-8, a body over 10 MiB or a middleware refusal, is answered with an uncached `307` to its upstream address. Redirects are written at the route's address where one serves their target. On a path a stylesheet entry covers, the reader's conditions are answered against what is delivered. Settings refuse a rehosting route with path rewriting, Image Optimizer, origin auth, a foreign `Host`, a directory index, a stripped query, a prefix under `/_ts` or one overlapping another route. +- The CSS address rewriter creatives use is shared with rehosted stylesheets, and media signing answers `If-Modified-Since` and `If-Unmodified-Since` as well as entity-tag conditions on the answers it delivers whole. +- Rehosting resolves URLs against the first effective base in the complete decoded original HTML, including references before that base and protocol-relative scheme selection. Rehosting and applicable middleware source-protection policies hold original HTML under the existing publisher body limit. HTML references are decoded once, and `srcset`/`imagesrcset` publisher fallback replaces only a leading authority or bounded bare host, preserving publisher names in other URL components. CSS-chain candidates bypass raw-copy caching; final CSS freshness is capped by remaining origin freshness and route policy after middleware. Empty rewritten redirects discard origin representation headers, and transparent routes validate the complete HTTP Host authority, including non-default ports. +- Encypher signs an inline `` as the standalone SVG document whose XML tree is the tree the browser's HTML parser built: the original bytes where they already parse as XML to that tree, otherwise that tree serialized as XML, kept only where the serialization parses back to the same tree both as HTML and as XML. Before any embedded-data edits, it protects every original provenance-bearing SVG and all of its descendants byte for byte, without submitting either the whole SVG or its children for signing (`svg_existing_provenance`). Uncertain or unclosed protected ranges stay protected through EOF. XML serialization enforces its output bound before every append and namespace insertion. The skip reason `inline_svg_not_standalone` is now `inline_svg_not_equivalent`; `svg_provenance_not_equivalent` remains the equivalence refusal for provenance-bearing input that cannot be preserved as written. +- Applicable middleware can protect exact original source ranges before core, fetch or serve changes. Encypher uses the same SVG classifier as its signing scan to protect provenance-bearing and structurally uncertain SVGs, including late carriers and descendants, even with no rehost routes. The bounded HTML pass holds opaque, collision-checked markers, restores original byte slices before publishing a shared template, and protects the reader copy again on cache replay. Lost, duplicated, reordered or altered markers, invalid classifier ranges, and input/masked/restored output limit excess refuse the pass rather than serving altered signed SVG. Unsigned SVG normalization/signing and surrounding page transformations remain enabled. +- Moved CSS keeps browser `max-age` separate from shared `s-maxage` (falling back to `max-age`). It subtracts the greater of received `Age` and apparent age from `Date`, plus processing time, before rebasing headers, then applies the corresponding browser and shared route caps separately. Streamed stylesheet collection uses the existing collector's pre-append capacity check; buffered bytes remain zero-copy. +- On a host that serves several publishers by path, through a deployment's request addressing, Encypher signs each article, embedded item and media file for the address the reader asked for, with the publisher's path prefix, where it named the address without the prefix, so two publishers' pages at one path no longer share an address. The path exclusions still read the publisher's own path. Module middleware sees the removed prefix on the publisher path as `ModuleRequest::removed_prefix`. +- The Axum, Cloudflare and Spin HTTP clients stream an origin response when the caller asks for one, as the Fastly client does, so `supports_streaming_responses` is true on every adapter. A streamed body is read one chunk at a time and never held whole. A caller's `max_response_body_bytes` is enforced exactly, chunk by chunk, in place of the buffered ceilings of 10 MiB on Cloudflare and 8 MiB on Spin. A declared `Content-Length` over the limit is refused before any byte is read, and an origin read failure, or a body cut short of its declared length on Cloudflare and Spin, ends the stream with an error instead of a clean end. A Spin streamed body keeps the origin's `Content-Encoding`, and the Workers runtime still decodes one. `send_async` stays buffered on these adapters and refuses a request that asks for a stream. Delivery streams too. The Axum dev server runs its own front end in place of `AxumDevServer`, whose service collected each streamed response into one buffer: it passes chunks to Hyper through a bounded channel, stops the origin read when the client leaves, and runs at most 256 requests at once. The Spin entry point dispatches through the router itself in place of `run_app`, whose conversion collected a streamed response up to 16 MiB, and hands the host one frame per chunk. +- Provenance coverage is limited to eligible responses and source-embedded media processed by configured routes and middleware. Direct third-party requests and script-created SVG remain outside that coverage. Local real-API and browser verification does not establish complete page coverage, public detached-manifest retrieval or a production deployment. +- **Breaking:** Encypher's signed media, and its record of each large-media sign it made or declined, are held through `assets`, a cache `provenance.encypher` declares, where core held them beside the origin's own copies. `[cache.assets]` keeps its four settings and their defaults (5 seconds in memory, an hour in the store, a day idle, never past the origin's freshness) and holds no raw origin copy, which an asset cache module selected in `[asset-cache]` holds. `module` must name a linked loading cache module, so a document that writes `module = "none"` is refused at validation and startup; remove the table to keep no copy in a build that links no loading cache module. - The Tinybird project, its datasources, pipes, fixtures and tests, moves from `tinybird/` to `crates/analytics/tinybird/project/`, beside the analytics module's Rust, so run `tb` from the new folder. - A vendor module's browser script and its tests live in the crate that supplies the module, under `js/src//` and `js/test//`, where they were under `crates/trusted-server-js/lib/src/integrations/` and `lib/test/integrations/`, for DataDome, Didomi, Google Tag Manager, GPT, GPT diagnostics, Lockr, Osano, Permutive, Sourcepoint and Testlight. The built bundles are unchanged. A script in a crate imports the shared browser code as `@tsjs/core/...` and `@tsjs/shared/...`, and another module's code as `@tsjs/module//...`. -- The Axum, Cloudflare and Spin adapters serve `[[proxy.asset_routes]]`, where only the Fastly adapter did, so a `GET` or `HEAD` under a route's prefix is answered from the route's source, with its signing and with the answers a selected asset cache module holds for it, and no longer by the publisher's origin. The response skips the Edge Cookie steps, so it neither writes nor expires the Edge Cookie, and takes the route's cache policy after the finalize headers. Those adapters hold the asset body whole, up to 10 MiB on Cloudflare and 8 MiB on Spin. Their HTTP clients refuse Image Optimizer metadata, which the Axum client now does on both of its send paths, so there a route that enables the Image Optimizer answers each request it would optimize with `502` and `no-store, private`, after a signed route has asked its origin for the object with a `HEAD`, and serves its SVG paths and requests that set the debug parameter unoptimized. Each adapter logs a warning naming every such route, once for each instance of the adapter, which on Axum is the process. +- The Axum, Cloudflare and Spin adapters serve `[[proxy.asset_routes]]`, where only the Fastly adapter did, so a `GET` or `HEAD` under a route's prefix is answered from the route's source, with its signing and with the answers a selected asset cache module holds for it, and no longer by the publisher's origin. The response skips the Edge Cookie steps, so it neither writes nor expires the Edge Cookie, and takes the route's cache policy after the finalize headers. Those adapters stream the asset body as the Fastly adapter does, and hold a buffered one whole, up to 10 MiB on Cloudflare and 8 MiB on Spin. Their HTTP clients refuse Image Optimizer metadata, which the Axum client now does on both of its send paths, so there a route that enables the Image Optimizer answers each request it would optimize with `502` and `no-store, private`, after a signed route has asked its origin for the object with a `HEAD`, and serves its SVG paths and requests that set the debug parameter unoptimized. Each adapter logs a warning naming every such route, once for each instance of the adapter, which on Axum is the process. - **Breaking:** on Spin, a request to a source whose `host_header` names a host other than the one in its `url` answers `502` before its origin is asked, the publisher's `[source.origin]` as well as an asset route's source, because Spin sends the host in `url` as the `Host`. Before, the request reached its origin with the host in `url` in place of `host_header`. A Spin deployment whose publisher source sets such a `host_header` answers every page with `502`, robots.txt included. Before upgrading, check that no source of a Spin deployment sets a `host_header` naming another host, or remove it once its origin answers requests that carry the host in `url` as the `Host`. - The Edge Cookie is written without an identity graph as it is with one, because the graph is one optional store of the data kept with the cookie, such as partner IDs. A Fastly service with no `[ec] ec_store` now writes the cookie a real browser's page navigation creates, and `POST /_ts/api/v1/ec/resolve` answers there as it does with a graph, where neither wrote one. So the identifier auction providers get as `user.id`, where storage and personalized-ad selection are both set, is one value for the life of the cookie, where a server-side module such as `hmac` gave a new value on each page view and a client-side module gave none. Naming an `ec_store` later gives each browser holding a cookie a server-side module wrote without one a new identifier on its first page view, because the identity graph replaces an identifier it has no row for. A client-side module creates nothing at the edge to replace its cookie with, so that cookie keeps its identifier with no row for as long as the browser keeps the cookie and its resolved marker, and each of that browser's page views pays for an extra identity-graph read and a strongly consistent existence check. **Breaking for crates outside core:** `ec_finalize_response` and `handle_ec_resolve` read the bot gate from the context's device signals (`EcContext::set_device_signals`), so a context with none gets no created cookie. - The Axum, Cloudflare and Spin adapters create and write the Edge Cookie and serve `POST /_ts/api/v1/ec/resolve`, where only the Fastly adapter did. A real browser's page navigation is given an Edge Cookie where its permissions allow one, and every route in the request cycle finalizes the Edge Cookie once the response is finalized, which writes a created cookie under the request's public prefix and path and expires one on a withdrawal. Those adapters open no identity graph, so the cookie is written with no graph row, by the resolve route too, the graph's syncs are skipped, and startup logs once that the store `[ec] ec_store` names is not opened. The bot gate there passes any client that sends a browser's `User-Agent`, and where storage and personalized-ad selection are both set a real browser's identifier reaches auction providers as `user.id`, from its first page view with a server-side module and from the requests after its resolve with a client-side one. @@ -49,7 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The Axum dev server and Spin, which have no geo lookup, refuse `[geo] module = "platform"` at startup naming the setting. The selection located no request there and still counted as a geo module, so a deployment running an Edge Cookie module was never asked to set `[geo] assume_single_jurisdiction`. The configuration and Cloudflare guides say that Cloudflare places a visitor in a region only from the `cf-region-code` request header. - A module declares the caches it loads through, by name, and a deployment selects a loading cache for each in its own `[cache.]` table, the way `[cache.robots-txt]` selects one for robots.txt. A `[cache.]` table naming a cache no linked module declares is refused by `ts config validate` and when the deployment starts, naming the ones it does, and a module declaring `robots-txt` or a name another module declares is refused. `IntegrationBuilder::with_cache_features` declares them, `CacheSettings` has a new public field `declared`, `CacheSettings::loading_caches` returns a `Vec` in place of an array, listing core's own and the declared ones, and `LoadingCaches::insert` takes any name. - **Breaking for crates outside core:** the cookie constants `COOKIE_TS_EC`, `COOKIE_TS_EC_RESOLVED`, `COOKIE_TS_EC_PULL_COMPLETE`, `COOKIE_TS_EIDS` and `COOKIE_TS_TESTER` are `COOKIE_EC`, `COOKIE_EC_RESOLVED`, `COOKIE_EC_PULL_COMPLETE`, `COOKIE_EIDS` and `COOKIE_TESTER`, holding the entry's name alone, and a cookie is named through the request's public prefix with `services.public_prefix().cookie_name(COOKIE_EC)`. A module's response header naming a cookie under the public prefix is refused as one under `ts-` was. -- A robots.txt module's answer is held by the loading cache of the one loading cache module the build links, or of the module `[cache.robots-txt] module` names, briefly in memory and in the host's shared store outside it, so a module is asked again only when no copy young enough is held, rather than by every request. With no loading cache module linked, or several and none named, nothing is held and every request for the file asks each module, as every request did when no adapter gave the file a key-value store. The last answer is kept apart for 30 days and served when the module cannot answer, in the value cache on Fastly and in the key-value store on Cloudflare and Spin, and Axum, which has no key-value store, keeps none. +- A robots.txt module's answer is held by the loading cache of the one loading cache module the build links, or of the module `[cache.robots-txt] module` names, briefly in memory and in the host's shared store outside it, so a module is asked again only when no copy young enough is held, rather than by every request. With no loading cache module linked, or several and none named, nothing is held and every request for the file asks each module, as every request did when no adapter gave the file a key-value store. The last answer is kept apart for 30 days and served when the module cannot answer, in the value cache on Fastly and in the key-value store on Cloudflare and Spin. Axum persists it in the redb file named by `TRUSTED_SERVER_KV_PATH`, shared with Encypher and other runtime KV consumers; with that variable unset, Axum keeps no persistent fallback. - **Breaking:** the Cloudflare and Spin adapters declare their key-value store, the Workers KV binding `TRUSTED_SERVER_KV` and Spin's `default` store, so EdgeZero opens it for every request, where every request was given a store whose every call failed, and a loading cache module that a deployment links keeps its copies there between requests. A declared store is required, so a Worker without the binding, or a Spin component that cannot open `default`, fails every request before any route runs. - A consent management platform's middleware runs in the phase the publisher chooses. `cmp.inmobi`, `cmp.didomi` and `cmp.sourcepoint` may be named in a `[[fetch]]` or a `[[serve]]` entry, where each ran in the fetch phase alone. A generated configuration still places them in `[[fetch]]`. A `[[serve]]` entry naming `cmp.inmobi` for a page whose `[[fetch]]` entry names another middleware is refused, because the serve phase writes its head markup after what the fetch phase writes ahead of the script bundle, so a vendor's tag there would come ahead of the prompt. Naming `cmp.inmobi` for one page in both phases is refused, because the prompt would be written twice. - **Breaking:** auction telemetry is selected the way every other module is. `[tinybird]` is now `[analytics.tinybird]`, and `[analytics] module = "tinybird"` selects it, which replaces the `enabled` line. A configuration that still carries `[tinybird]` is refused with directions. A stored configuration written by an earlier version carries the table even where telemetry was off, so push the configuration again with this version. The secret reference is `analytics.tinybird.auction_token_secret`, the table refuses a key it does not know, and a section that selects nothing, or a module this build does not have, is refused. A row is sent only for a reader whose permissions include `analytics.ad_reporting.measure_ad_performance`, so none is sent for a reader the consent gate refuses unless they permit it. The Axum, Cloudflare and Spin adapters send no telemetry, and log a warning at startup when the section selects a module. @@ -112,6 +132,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +- Encypher now applies `max_document_bytes` to the exact signed HTML size before allocating output, including every marker and the escaped manifest meta element. An API plan that would expand a page past the publisher buffer limit now leaves the original page unchanged instead of making buffered delivery fail. Policy and signing calls bypass platform intermediary caches and carry a receive-time body bound, enforced chunk by chunk by each adapter before it buffers or yields the crossing chunk. +- Encypher stores positive plans and policy digests separately from short-lived failures. A delayed failed request can no longer replace a concurrent successful result. Malformed and structurally invalid cache entries are deleted best effort so the next view can recover, while a valid plan that does not align with one HTML representation remains available to other representations. - With `consent_forwarding = "openrtb_only"`, the Model Terms for Marketing preference cookie, `__mtm_pref`, is withheld from Prebid Server along with the consent cookies. It was forwarded in every mode. - A response no longer carries `X-Served-By`, `X-Cache` or `X-Cache-Hits`, unless this server answered a request for a path beneath `/_ts`. The three name the node that answered and say how a cache treated the request, and arrive from the publisher's origin or a cache on the way to it. Every adapter removes them before it finalizes a response, so one set in `[response_headers]` is still sent. - No adapter writes a version on a response. A deployment that wants the versions on its pages names core's `service_version` middleware in a `[[serve]]` entry, which writes `X-TS-Version` from the git version the build compiled in and `X-TS-Service-Version` from the version the adapter reports. @@ -138,6 +160,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Encypher `execution = "async_prepared"`, opt-in with `[provenance.encypher.preparation]`, moves signing out of reader delivery into a publisher-operated native worker, `trusted-server-axum preparation-worker --config worker.toml`, which owns a durable redb store. Delivery only looks up results bound to the exact item (article text with its title, or media bytes), page URL, admitted variant, policy and credential, and sends a miss unsigned; it never falls back to synchronous signing, which stays the default. The worker prepares supplied targets from `ts preparation warm --target ... [--register] [--retry]`, its `[[inventory]]` and optional bounded reader hints (`reader_trigger`), hashes media locally without uploading it, and reports through `ts preparation status`; `ts preparation retire` publishes durable retirement markers. It does not crawl or discover uploads. Axum, Cloudflare and Spin WASM builds bound hints with a timer, Fastly sends them after the response, and native Cloudflare and Spin builds refuse `reader_trigger = true`. `TS_LISTEN_ADDR` sets the Axum listener address. See [Encypher prepared signing](docs/guide/configuration.md#encypher-prepared-signing). +- Encypher article provenance is available on Fastly, Cloudflare, Spin and Axum. Select `[provenance] module = "encypher"` and place `provenance.encypher` in a `[[serve]]` entry for public article routes. Signing failures pass the held page through unchanged; origin-private and Set-Cookie responses are excluded. Signing requires a runtime KV store: link Fastly's `trusted_server_kv`, bind Cloudflare's `TRUSTED_SERVER_KV`, declare Spin's `default` store, or set Axum's `TRUSTED_SERVER_KV_PATH`. See the [Fastly deployment prerequisites](docs/guide/fastly.md) and [Spin storage and retention guidance](docs/guide/spin.md). - `ts config migrate --adapter ` rewrites a settings document written for an earlier release, such as one written before the breaking changes above, into the shape this release reads, and prints the result. The host's location lookup the earlier release ran becomes `[geo] module = "platform"` on Fastly and Cloudflare and no geo module on Axum and Spin, which have no lookup. On stderr it lists each change it made, each setting it left for the operator because it needs a decision or a value the document does not hold, such as `[geo] assume_single_jurisdiction` for an Edge Cookie module with no geo module, and each change in behavior no setting expresses, then checks the result with the settings validation `ts config validate` runs. It keeps the comments and the order of the tables, comments out a module that was not enabled under its new name, never selects `headers.geo`, which writes the reader's location, never prints a value from the document, rewrites the file in place with `--write`, and with `--check` changes nothing and exits 1 when the document needs migrating. Migrating a migrated document changes nothing. - A 51Degrees client hints middleware, `51degrees.client_hints`, from `crates/middleware/51degrees`, run by `[shared.51degrees]` and placed by a `[[serve]]` entry. It asks the 51Degrees service which client hints the reader's browser can send for each page navigation an entry covers, sending the `User-Agent` and `Sec-CH-UA` hints alone, and leaves the hint names in the request's state for the other 51Degrees modules, which keeps a page it names hints for out of the stored templates and sends it `private, no-store`. On each page an entry covers it writes `Accept-CH` and `Critical-CH` on the response unless the browser already sends every one of those hints, `Permissions-Policy` and the `Delegate-CH` tag that delegate those hints to the service's origin, each keeping every line the response already carries. `critical` in `[middleware.51degrees.client_hints]` turns the retry of the first navigation off. The service's answers load through the cache `51degrees-answers`, selected and sized in `[cache.51degrees-answers]`, and are held only where the build links a loading cache module, so a stock build asks the service for each page navigation an entry covers. What the 51Degrees modules share, being the table, the one call to the service, the evidence it sends and the cache its answer is held in, is the new `crates/shared/51degrees`. - `[middleware.]`, a middleware's own settings table, written with the middleware's full name and read through `Settings::middleware_config`. A table holds values and arrays, a table within one is the longer name's table and never a setting, and a table at a name no running module supplies is refused at startup with the names that could be written. @@ -154,7 +178,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `robots_txt`, core's own middleware for the file `[robots-txt]` describes, named in a `[[fetch]]` entry for `text/plain` on `/robots.txt`. Its preparation makes the file from the selected modules through their holding, and its action replaces the origin's file with it. The file is made in place of any other answer the origin gives to a `GET` or `HEAD`, such as a `404`, a page, an error or a redirect. The origin is asked for its whole file, without a crawler's conditional or range headers, and the middleware works on `/robots.txt` alone. A module that cannot answer with nothing held refuses the request with `503` and `Retry-After` before any document is processed. A section with no such entry, or with an entry naming the middleware on another path or media type, is refused when the settings load. - A middleware may say which request paths it works on, with `Middleware::handles_path`, that it writes the whole document, with `writes_whole_document`, so core makes the document in place of any answer it would not hand the middleware as a `200` of the entry's media type, and that it needs the origin's whole body, with `needs_whole_body`, so the origin is asked without the reader's conditional and range headers. By default a middleware works on every path its entry covers and says neither of the other two. - A middleware may prepare, and may refuse. `Middleware::prepare` runs in the request's handler before the document is processed, as the middleware's module with the request's services, and what it answers is kept under the phase and the middleware's name for `create` in that phase, so a decision that needs a store or the network is made there while `create` stays synchronous. The fetch phase of a document that may be stored is prepared with no client IP, TLS details or client signals in the services. A refusal answers the request with its status and `Retry-After`, with no document, and nothing is stored. Core logs a refusal as a warning naming the middleware, and answers a status that is not a client or server error with `500`. -- A loading cache. `PlatformLoadingCache` in core loads a missing value once per key however many requests ask for it at the same moment, gives every waiting request the value or the error, and holds nothing that failed. A load is handed services with nothing of the reader in them, from `RuntimeServices::without_connection`, because its value serves every reader. A crate offers one through `IntegrationBuilder::with_loading_cache`, each feature that holds answers selects the module whose cache holds them in its own `[cache.]` table, and an adapter takes the selections with `loading_caches_from`. A feature that selects none takes the one loading cache module the build links, and gets `NoLoadingCache`, which loads every time, when the build links none or several. The new `crates/loading-cache/51degrees`, the module `loading-cache.51degrees`, implements it with the published `fiftyone-caching` package as a brief copy in memory over the host's value cache on Fastly or its key-value store on Cloudflare and Spin, with the memory copy alone on Axum, for a deployment that links it, because no stock build does. Each application state keeps a memory copy of its own, which lasts one request where an adapter builds its state for each request, as Cloudflare and Spin do. Every load runs in its caller. +- A loading cache. `PlatformLoadingCache` in core loads a missing value once per key however many requests ask for it at the same moment, gives every waiting request the value or the error, and holds nothing that failed. A load is handed services with nothing of the reader in them, from `RuntimeServices::without_connection`, because its value serves every reader. A crate offers one through `IntegrationBuilder::with_loading_cache`, each feature that holds answers selects the module whose cache holds them in its own `[cache.]` table, and an adapter takes the selections with `loading_caches_from`. A feature that selects none takes the one loading cache module the build links, and gets `NoLoadingCache`, which loads every time, when the build links none or several. The new `crates/loading-cache/51degrees`, the module `loading-cache.51degrees`, implements it with the published `fiftyone-caching` package, for a deployment that links it, because no stock build does, as a brief copy in memory over the host's value cache on Fastly or its key-value store on Cloudflare and Spin. Axum uses the persistent runtime KV file when `TRUSTED_SERVER_KV_PATH` is set and only the memory copy when unset. Each application state keeps a memory copy of its own, which lasts one request where an adapter builds its state for each request, as Cloudflare and Spin do. Every load runs in its caller. - `[cache.robots-txt]` selects, with `module`, the loading cache that holds each robots.txt module's answer, and sets the lifetimes of its copies, in seconds: `memory_seconds` (default 60), `store_seconds` (default 604800) and `store_idle_seconds` (default 2592000). A memory lifetime may be 0, the others must be at least 1, and none may be more than a year. A `module` no linked crate offers a cache from is refused by `ts config validate` and when the service starts. - A stock build links no loading cache module, so every feature loads through `NoLoadingCache`, which keeps no copy, and `module = "51degrees"`, or any other `module`, is refused by `ts config validate` and when the service starts. A deployment whose document names the module links it first, by handing `trusted_server_loading_cache_51degrees::builder()` to its adapter through `run_with` on Fastly or `routes_with_registrations` on Axum, Cloudflare and Spin. - `ServerKeyPrefix`, the prefix every loading cache key starts with, being `ts.` and the publisher's domain with a leading `www.` dropped, so publishers sharing a store never share an entry. diff --git a/Cargo.lock b/Cargo.lock index 1a1357a8e7..fcc2346f07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4838,7 +4838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools 0.10.5", + "itertools 0.13.0", "log", "multimap", "once_cell", @@ -4858,7 +4858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.118", @@ -4871,7 +4871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.118", @@ -5265,6 +5265,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "roxmltree" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907" +dependencies = [ + "memchr", +] + [[package]] name = "rpassword" version = "7.5.4" @@ -5992,6 +6001,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string-interner" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0" +dependencies = [ + "hashbrown 0.15.5", + "serde", +] + [[package]] name = "string_cache" version = "0.8.9" @@ -6626,9 +6645,11 @@ dependencies = [ name = "trusted-server-adapter-axum" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "axum", "base64", + "bytes", "edgezero-adapter-axum", "edgezero-core", "error-stack", @@ -6665,6 +6686,8 @@ dependencies = [ "trusted-server-permission-signal-mtm", "trusted-server-permission-signal-tcf", "trusted-server-permission-signal-us-privacy", + "trusted-server-preparation-worker", + "trusted-server-provenance-encypher", "trusted-server-shared-aws", "trusted-server-tag-google-tag", "trusted-server-tag-google-tag-manager", @@ -6676,6 +6699,7 @@ dependencies = [ name = "trusted-server-adapter-cloudflare" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "base64", "bytes", @@ -6713,6 +6737,7 @@ dependencies = [ "trusted-server-permission-signal-mtm", "trusted-server-permission-signal-tcf", "trusted-server-permission-signal-us-privacy", + "trusted-server-provenance-encypher", "trusted-server-shared-aws", "trusted-server-tag-google-tag", "trusted-server-tag-google-tag-manager", @@ -6769,6 +6794,7 @@ dependencies = [ "trusted-server-permission-signal-mtm", "trusted-server-permission-signal-tcf", "trusted-server-permission-signal-us-privacy", + "trusted-server-provenance-encypher", "trusted-server-shared-aws", "trusted-server-tag-google-tag", "trusted-server-tag-google-tag-manager", @@ -6790,6 +6816,7 @@ dependencies = [ "error-stack", "flate2", "futures", + "http-body", "http-body-util", "log", "serde_json", @@ -6820,6 +6847,7 @@ dependencies = [ "trusted-server-permission-signal-mtm", "trusted-server-permission-signal-tcf", "trusted-server-permission-signal-us-privacy", + "trusted-server-provenance-encypher", "trusted-server-shared-aws", "trusted-server-tag-google-tag", "trusted-server-tag-google-tag-manager", @@ -7066,6 +7094,7 @@ dependencies = [ "trusted-server-headers-geo", "trusted-server-identity-lockr", "trusted-server-middleware-51degrees", + "trusted-server-provenance-encypher", "trusted-server-shared-aws", "trusted-server-tag-google-tag", "trusted-server-tag-google-tag-manager", @@ -7159,6 +7188,7 @@ dependencies = [ "glob", "hex", "hmac", + "html5ever", "http", "httpdate", "jose-jwk", @@ -7399,6 +7429,60 @@ dependencies = [ "trusted-server-permission-signal-gpc", ] +[[package]] +name = "trusted-server-preparation-worker" +version = "0.1.0" +dependencies = [ + "axum", + "base64", + "bytes", + "derive_more", + "http", + "log", + "rand 0.8.6", + "redb", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2 0.11.1", + "tempfile", + "tokio", + "toml", + "trusted-server-provenance-encypher", + "url", +] + +[[package]] +name = "trusted-server-provenance-encypher" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64", + "bytes", + "derive_more", + "ed25519-dalek", + "edgezero-core", + "ego-tree", + "error-stack", + "futures", + "hmac", + "html5ever", + "http", + "log", + "roxmltree", + "scraper", + "serde", + "serde_json", + "sha2 0.11.1", + "subtle", + "trusted-server-core", + "url", + "validator", + "wasmi", + "wat", + "web-time", +] + [[package]] name = "trusted-server-shared-51degrees" version = "0.1.0" @@ -7860,7 +7944,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.247.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser 0.262.0", ] [[package]] @@ -7871,8 +7965,8 @@ checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" dependencies = [ "anyhow", "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.247.0", + "wasmparser 0.247.0", ] [[package]] @@ -7888,6 +7982,56 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78693fcdd618e0fc34af59c6b8efa9ac5d58c68df940beff4bedddb6acfe7c27" +dependencies = [ + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser 0.228.0", +] + +[[package]] +name = "wasmi_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a8be2aa467cf2d29e96ff759472c36eeb44a3c81c67fc9cb76c9a24c519c557" +dependencies = [ + "string-interner", +] + +[[package]] +name = "wasmi_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69372d5fda3ea3d1e0aa6603c7888110e0187e88ea17cd8fc2e2df0a0e1f37fa" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f17b774caa13c618c7244f1ee51fe23c5e7b8538a471fa46d9949779758aed6" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.228.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3" +dependencies = [ + "bitflags 2.13.0", + "indexmap 2.14.0", +] + [[package]] name = "wasmparser" version = "0.247.0" @@ -7900,6 +8044,39 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags 2.13.0", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "wast" +version = "262.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30612a0c29249b5f185888a107e8a519d920226b7d07f9d351fd094e9f75eb23" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.262.0", +] + +[[package]] +name = "wat" +version = "1.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228027f2b07bbaf946980ebbe2437fca73be6f54c40c2a8d4f42f277075f8d1" +dependencies = [ + "wast", +] + [[package]] name = "web-sys" version = "0.3.103" @@ -8310,9 +8487,9 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.247.0", "wasm-metadata", - "wasmparser", + "wasmparser 0.247.0", "wit-parser", ] @@ -8332,7 +8509,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.247.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7cf578fafb..f3b83d4258 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "crates/ads-txt/iabtl", "crates/analytics/tinybird", "crates/headers/geo", + "crates/provenance/encypher", "crates/loading-cache/51degrees", "crates/shared/aws", "crates/shared/51degrees", @@ -47,6 +48,9 @@ members = [ "crates/trusted-server-integration-tests", "crates/trusted-server-js", "crates/trusted-server-openrtb", + # Native-only publisher preparation worker; the Axum adapter launches it. + # No wasm alias names it. + "crates/trusted-server-preparation-worker", # Manual protobuf codegen tool — a member so it shares workspace settings, # but no default member or alias builds it. Run it explicitly with `-p`. "crates/trusted-server-openrtb-codegen", @@ -99,6 +103,8 @@ edgezero-adapter-fastly = { git = "https://github.com/IABTechLab/edgezero", tag edgezero-adapter-spin = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.10", default-features = false } edgezero-cli = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.10" } edgezero-core = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.10", default-features = false } +# The tree scraper's HTML sink builds; 0.10 is the version scraper locks. +ego-tree = "0.10" env_logger = "0.11" error-stack = "0.6" esi = "0.7.2" @@ -116,8 +122,12 @@ glob = "0.3" getrandom = "0.2" hex = "0.4.3" hmac = "0.13" +# The HTML parser scraper drives; the Encypher module parses inline SVG +# fragments with it directly. 0.35 is the version scraper already locks. +html5ever = "0.35" http = "1.4.0" httpdate = "1.0.3" +http-body = "1" http-body-util = "0.1" hyper = "1" hyper-util = "0.1" @@ -134,8 +144,11 @@ predicates = "3" prost-build = "0.13" rand = "0.8" rcgen = { version = "0.13", features = ["x509-parser"] } +redb = "4.1" regex = "1.12.3" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +# The Encypher module's XML parser for inline SVG; DTDs stay refused. +roxmltree = { version = "0.21.1", default-features = false, features = ["std"] } rpassword = "7.5" rustls = "0.23" rustls-pemfile = "2" @@ -176,6 +189,8 @@ trusted-server-cmp-inmobi = { path = "crates/cmp/inmobi" } trusted-server-ads-txt-iabtl = { path = "crates/ads-txt/iabtl" } trusted-server-analytics-tinybird = { path = "crates/analytics/tinybird" } trusted-server-headers-geo = { path = "crates/headers/geo" } +trusted-server-provenance-encypher = { path = "crates/provenance/encypher" } +trusted-server-preparation-worker = { path = "crates/trusted-server-preparation-worker" } trusted-server-loading-cache-51degrees = { path = "crates/loading-cache/51degrees" } trusted-server-shared-aws = { path = "crates/shared/aws" } trusted-server-shared-51degrees = { path = "crates/shared/51degrees" } @@ -198,6 +213,10 @@ url = "2.5.8" urlencoding = "2.1" uuid = { version = "1.18", features = ["v4"] } validator = { version = "0.21", features = ["derive"] } +# The Encypher provenance module's probe host: an interpreter with no JIT, +# so the signed probe release runs on every edge target. +wasmi = { version = "2.0", default-features = false, features = ["std", "validate", "auto-dispatch"] } +wat = "1.247" web-time = "1" webpki-roots = "1" which = "8" diff --git a/PRDs/ARCHIVE/trusted-server-v2-encypher.md b/PRDs/ARCHIVE/trusted-server-v2-encypher.md new file mode 100644 index 0000000000..0252d17670 --- /dev/null +++ b/PRDs/ARCHIVE/trusted-server-v2-encypher.md @@ -0,0 +1,263 @@ +# Trusted Server v2: Encypher provenance as a module + +## Status + +Internal completion review cleared at bar 9.5 in cycle 2. Review corrections +remain local on `encypher/v2-provenance-module`, based on +`feature/55-scripts-in-crates` at `5be114773`. +The existing draft https://github.com/IABTechLab/trusted-server/pull/1448 +still contains the earlier published revision, not these corrections. +Successful live API signing remains unverified; in-process publisher signing +passes against a controlled API. Clearance covers the implementation contract, +not deployment admission or live demo readiness. + +## Current Goal + +Internal review complete. Preserve the publication hold and hand off the local +corrections and separate live-signing verification prerequisite. No landing, +deployment, comment or email was performed during this review. + +## Review Loop State + +bar: 9.5 max-cycles: 5 worktree: /home/agentdesk/code/trusted-server-worktrees/v2-provenance +resumed at: Phase 5 (finalization sweeps + completion gate B) +plan gate: NOT PERFORMED. Gate A was never run on this PRD. It is not +retroactively cleared and must not be recorded as cleared. +completion gate: CLEARED (cycle 2). +Cycle 1 NOT CLEARED: +Scores (GPT-5.5 / Opus): correctness 9.0 / 9.1; simplification 9.0 / 9.0; +security 10.0 / 9.6. Both reviewers accepted all 38 CI commands passing in +run `1792869014289`. Medium findings were stale Axum persistence wording +and whole-article canonical/fragment allocation beyond the signed prefix. +Cycle 2 CLEARED: correctness 10.0 / 9.5; simplification 10.0 / 9.6; +security 10.0 / 9.6. Every verdict is pass. Final full gate `1792871494395` +passed all 38 commands; final rebuilt Axum runtime smoke passed the exercised +fail-open and private-response exclusion scenarios. No high or medium finding +remains. Opus retains the live-signing evidence gap and accepted two-pass +boundary/metadata separation as low findings. + +Publication hold: no pushes, remote PR changes, comments, email or deployments. +All review corrections remain local; no landing requested. + +Independent pre-score assessments: `openai-think` (GPT-6 Astra) and +`anthropic-think` (Claude Fable 5) completed and identified the initial corrections. +The local finalization corrections now cover raw-markup boundary selection, +entity decoding, output expansion, negative-cache overwrite races, invalid +cache recovery, adapter receive-time response limits and credential-bound +HTTP cache bypass. The matching server contract also confirms that equal +canonical text can reuse the first URL-bearing manifest across URL or title +changes. Final pre-score review additionally corrected raw-text scan complexity, +full-article insertion-offset allocation and canonical ignored custom-element +prefix handling. Full local gates `1792869014289` and `1792870296283` passed all +38 commands. Cycle 2 additionally removes full-fragment materialization, +repeated metadata scans and the second rendered-document allocation. Its +93 module tests and module clippy pass, as does the final 38-command full gate. + +## Overview + +Encypher's text provenance contribution signed article pages from inside core, +through an HTML stream processor factory, a request context type and core +changes of its own. The stack in #777 moves every vendor out of core and lets a +module change a page only through a middleware. This work moves the +contribution onto those interfaces as `crates/provenance/encypher` +(`provenance.encypher`) and adds the neutral core seams it needs. Design: +`docs/superpowers/specs/2026-10-24-encypher-provenance-module-design.md`. + +## Objectives + +1. Port every existing Encypher behavior (gates, policy and plan caches, sign + call, render, fail-open, logging) to a vendor-owned module crate. +2. Add only vendor-neutral core seams: an awaited asynchronous finish for + middleware stream processors, an optional platform timer, response facts + for middleware, runtime KV on the adapters that lacked it. +3. Remove obsolete paths in the port: the `enabled` flag, `path_prefixes`, + `[integrations.encypher]`, the stream processor factory, the shared-template + refusal and the synchronous publisher body path. +4. Keep the permission contract: no permission required, no gated value read, + no page the origin marks private sent. +5. Publish a reviewable pull request against the stack head without changing + any upstream branch. + +## Work Breakdown + +Implementation (completed; evidence historical, see Completion Notes): + +1. [x] Inspect epic #777, PR #1423 head and the existing Encypher branches +2. [x] Create an isolated worktree and branch from 5be114773 +3. [x] `PlatformTimer` on `RuntimeServices`, with neutral documentation +4. [x] Fastly opens the KV store linked as `trusted_server_kv` +5. [x] `StreamProcessor::async_finish` and `finish`, composed in order by + every composite processor +6. [x] Every awaiting publisher path ends its processor with `finish`; the + synchronous `stream_publisher_body` path removed +7. [x] `MiddlewareContext::status` and `origin_private`, read before core's + cache policy +8. [x] Axum runtime KV from `TRUSTED_SERVER_KV_PATH`, on the blocking pool +9. [x] Timers on Cloudflare, Spin and Axum +10. [x] `crates/provenance/encypher`: config, secret setting, middleware, + ported processor and tests, publisher-path tests +11. [x] Module offered on all four adapters and the CLI +12. [x] Auction-hold regression test with an asynchronously finished document +13. [x] Documentation: configuration guide, Axum guide, example config, design +14. [x] Data-handling wording narrowed to what the gates prove +15. [x] Repository CI gates run locally once (historical; some adapter aliases + ran before the later rustfmt-only change and the auction-hold test) +16. [x] Axum runtime smoke with a controlled origin (fail-open and + origin-private paths only; the live signing endpoint was unavailable, so + live signing was not exercised) +17. [x] Draft pull request opened and its body updated with evidence + +Review loop, Phase 5 (complete; Gate B cleared at 9.5): + +18. [x] PRD finalization for the review loop (historical evidence retained; + premature completion removed; local Prettier check passed) +19. [x] Concise documentation sweep; timeout, retention and Axum persistence + wording corrected, with module deployment release notes +20. [x] Simplification sweep (behavior-preserving, covering tests named) +21. [x] Security sweep: entry-to-sink, fail-open, private and tenant cache + boundaries, finish ordering and cancellation, timers and budgets, + credential treatment, config and runtime registration, cache integrity +22. [x] Fresh verification: final full gate `1792871494395`, all 38 commands + passed, including 93 module tests and 3,022 core tests. Rebuilt Axum + runtime smoke verified fail-open and private/Set-Cookie exclusion +23. [x] Completion gate (Gate B) cleared at bar 9.5 by both reviewers on + correctness, simplification and security; cycle 2 scores above + +## Success Criteria + +| Criterion | Result | +| ----------------------------------------- | -------------------------------------------------------------------------------------------------- | +| Signing works through the publisher path | Met in process: buffered and streaming finalizers sign against a stub origin and stub API | +| Every failure passes the page through | Signed-output expansion regression passes for fresh and cached plans; original held bytes retained | +| A private origin page is never sent | Met: in-process test and runtime smoke `gate=origin_private` | +| Core platform-name neutrality lint passes | Met: `scripts/lint-core-platform-names.py` exit 0 | +| CI gates green | Final full run `1792871494395`: all 38 commands passed | +| Live API signing | Not met: not exercised, see limits | + +The internal completion gate cleared. Live signing remains a separate +deployment prerequisite rather than an inferred result of these checks. +In-process signing against the stub API does not prove interoperability. +The matching policy-digest implementation was located in the original commercial +`trusted-server-provenance` worktree at `bfe8f050` (policy change `832ded4b`). +The deployed API revision and current signing admission remain unverified. + +## Completion Notes + +Tests were ported from the earlier Encypher branches and written alongside the +port. The work was not strictly test-first, and no baseline run of the stack +was recorded before the change. + +The command runs below are historical, from the implementation work. Fresh +review evidence is recorded separately under Current review verification. + +Commands run in the worktree, with exit codes: + +- `cargo fmt --all -- --check`: 0 +- `cargo clippy-fastly`, `clippy-axum`, `clippy-cloudflare`, + `clippy-cloudflare-wasm`, `clippy-spin-native`, `clippy-spin-wasm`, + `clippy-cli`, `clippy-codegen`: 0 each +- `cargo test -p trusted-server-provenance-encypher`: 65 passed +- `cargo test -p trusted-server-core --lib`: 3020 passed; the auction-hold + regression test added after it: 1 passed +- `cargo test-fastly`, `test-axum`, `test-cloudflare`, `test-spin`: no + failures (run before the rustfmt-only change and the auction-hold test) +- `cargo check-cloudflare`, `check-spin`, `build-fastly`: no errors +- `cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity`: 0 +- `scripts/lint-wasm-clock.sh`: 0 +- `python3 scripts/lint-core-platform-names.py`: 0 +- `cd crates/trusted-server-js/lib && npx vitest run`: 0; `npm run format`: 0 +- `cd docs && npm run format`: 0; prettier check of markdown outside `docs/`: 0 +- `cargo test -p trusted-server-core --test template_build_digest`: 0; its + clippy with `-D warnings`: 0 +- docs-proposal: `uvx --from shellcheck-py shellcheck scripts/docs-proposal/*.sh` + (shellcheck is not installed on the host): 0; + `node --test scripts/docs-proposal/hunks.test.mjs`: 0; + `scripts/docs-proposal/test.sh`: 0 + +Runtime smoke, Axum dev server, throwaway script and mock origin, no real +credentials. The module was selected with a `[[serve]]` entry on `/news/`, +runtime KV on, and `[geo] assume_single_jurisdiction = true`, which the stack +requires once `[ec] module` is set. Observed: + +```text +[edgezero] config validate (typed): edgezero.toml + trusted-server.toml OK (strict) +page=public status=200 harbor_text=1 marker=0 +page=private status=200 harbor_text=1 marker=0 +INFO [trusted_server_provenance_encypher] encypher: registered for domain=publisher.example.com +INFO [trusted_server_provenance_encypher::processor] encypher: outcome=passed_through reason=http_error +DEBUG [trusted_server_provenance_encypher] encypher: document not held, gate=origin_private +``` + +`scripts/smoke-axum.sh` itself does not set the `[geo]` value and stops at +that check. This branch leaves `scripts/` and the core settings, config and +Edge Cookie code unchanged from 5be114773; the script was not run on the base +commit. + +Limits: + +- Live API: the runtime smoke pointed the module at a signing endpoint that + was not available, to exercise fail-open. Successful signing was tested in + process only, against a stub API. The live Encypher API was not exercised. +- Personalized pages: the module skips pages whose origin set a cookie or + forbade shared caching, and cannot detect others. A deployment places it on + public article routes. +- Spin's declared KV store also holds the settings. The module's four + positive and negative namespaces are versioned, but Spin has no item TTL. + Expired entries fail closed on read and are deleted best effort; abandoned + content keys require the namespace-retirement procedure in the Spin guide. +- Fastly has no guest timer. Its backend timeout bounds the first byte and + between-byte gaps, not total elapsed time, so `max_hold_ms` is not a total + deadline there. A trickling signing peer can exceed it. +- Equal canonical article text intentionally shares an Encypher plan across + page URL or title changes. The manifest can retain the first canonical URL, + as allowed by the matching server contract. + +### Current review verification + +The unattended gate is +`bun /home/agentdesk/scratch/team554-review-ci.mjs full`. +Run `1792867884379` passed 37 of 38 commands, including module/core and all +adapter tests, native/WASM builds and checks, CLI/parity, JavaScript, docs and +proposal checks. `clippy-spin-wasm` rejected an obsolete response-policy +wrapper; it was removed and its tests migrated to the existing bounded helper. +Subsequent runs `1792869014289` and `1792870296283` passed all 38 commands. +Final run `1792871494395` passed all 38 commands, including 93 module tests, +3,022 core tests, every adapter test/build/lint gate and documentation checks. +Shellcheck also passed separately. A superseded in-progress run was cancelled +before the final offset-vector preallocation correction; it is not a pass. + +Review regressions reproduced canonical custom-element text divergence. +The raw-text decoy regression passed functionally but took 5.78 seconds for +10,000 unrelated closing tags before correction. Sparse embedding over +4.9 million visible article characters passed functionally but used a +document-sized offset vector; observed process max RSS was 69,693,440 bytes. +The corrections filter unrelated raw end names before scanning attributes and +stop offset decoding at the highest signed index. No fixtures were changed. + +Rebuilt Axum smoke observed public, private and Set-Cookie responses with +status 200 and original article text preserved. Private and Set-Cookie paths +made no API connection. A stalled TLS handshake failed open with `timeout` +in 0.365 seconds on the final rebuilt binary. The scoped local CA was rejected despite passing +the Python trust control; successful signing, query omission and HTTP 503 +handling were not exercised by this runtime smoke. No trust checks were +weakened. Ports and temporary certificate keys were cleaned. + +Cycle 2 replaces whole-article fragment vectors and post-allocation text +truncation with a stoppable shared scanner and canonical visitor. Extraction +stops at 50,000 scalars; title decoding stops at 500 and occurs only on a sign +miss. One metadata pass saves manifest presence, title range and head-close +offset. Rendering merges markers and escaped manifest metadata in one +exact-capacity output allocation. Equal-offset markers precede metadata; +characterization tests preserve malformed-head, absent-head and empty-URL +behavior. Fixtures remain unchanged. + +Final evidence is retained under +`/home/agentdesk/scratch/team554-review-evidence/`, with final CI rows and +per-command logs keyed by `1792871494395` and runtime results under `runtime/`. +The security audit is under +`/home/agentdesk/security-audits/trusted-server/team554-review/`. +No source or fixture changes followed the final gate. This acceptance record +was finalized and archived locally afterward; no publication was authorized. +Historical test-first and plan-gate deviations remain recorded above. +The earlier outbound email attempt remains uncertain and must not be blindly +retried; see the TEAM_554 company session record. diff --git a/crates/middleware/51degrees/src/lib.rs b/crates/middleware/51degrees/src/lib.rs index d2a2e3ef82..6e5efb71d8 100644 --- a/crates/middleware/51degrees/src/lib.rs +++ b/crates/middleware/51degrees/src/lib.rs @@ -658,6 +658,9 @@ mod tests { request_host: "www.example.com", request_scheme: "https", origin_host: "origin.example.com", + status: http::StatusCode::OK, + origin_private: false, + response_headers: None, platform: test_support::platform(), document_state: state, max_buffered_script_bytes: 1 << 20, diff --git a/crates/provenance/encypher/Cargo.toml b/crates/provenance/encypher/Cargo.toml new file mode 100644 index 0000000000..6cfa8cde2b --- /dev/null +++ b/crates/provenance/encypher/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "trusted-server-provenance-encypher" +description = "Encypher provenance: signs the article on each page a [[serve]] entry names it for, with invisible markers and a C2PA manifest link, and the exact bytes of its media, delivering every item it cannot sign as it was and reporting it unsigned." +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +publish = { workspace = true } +version = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +async-trait = { workspace = true } +base64 = { workspace = true } +bytes = { workspace = true } +derive_more = { workspace = true } +ed25519-dalek = { workspace = true } +edgezero-core = { workspace = true } +ego-tree = { workspace = true } +error-stack = { workspace = true } +futures = { workspace = true } +hmac = { workspace = true } +html5ever = { workspace = true } +http = { workspace = true } +log = { workspace = true } +roxmltree = { workspace = true } +scraper = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +subtle = { workspace = true } +trusted-server-core = { workspace = true } +url = { workspace = true } +validator = { workspace = true } +wasmi = { workspace = true } +web-time = { workspace = true } + +[dev-dependencies] +trusted-server-core = { workspace = true, features = ["test-utils"] } +wat = { workspace = true } + +# The visible owner of this vendor crate. +[package.metadata.maintainers] +owner = "Encypher" +status = "vendor owned" diff --git a/crates/provenance/encypher/src/api.rs b/crates/provenance/encypher/src/api.rs new file mode 100644 index 0000000000..ddf89b92a7 --- /dev/null +++ b/crates/provenance/encypher/src/api.rs @@ -0,0 +1,303 @@ +use serde::{Deserialize, Serialize}; + +/// A 2 MiB cap accommodates roughly 600 KiB surrogate-escaped plans at the 50,000-scalar input limit. +pub const MAX_RESPONSE_BYTES: usize = 2 * 1024 * 1024; +/// Maximum visible-text length sent for signing, measured in Unicode scalars. +pub const MAX_TEXT_SCALARS: usize = 50_000; +/// Policy responses contain only one digest; cap them independently of plans. +pub const MAX_POLICY_RESPONSE_BYTES: usize = 1024; + +/// JSON request body for the Encypher CDN signing endpoint. +#[derive(Serialize)] +pub struct SignRequest<'a> { + /// Canonical visible article text. + pub text: &'a str, + /// Absolute page URL without its query string. + pub page_url: &'a str, + /// Optional page title capped at 500 Unicode scalars. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_title: Option<&'a str>, + /// Boundary detection label used for the article. + pub boundary_selector: &'a str, + /// Server policy used to select this article's cache namespace. + pub expected_policy_digest: &'a str, +} + +/// One invisible-marker insertion returned by the signing endpoint. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PlanOperation { + /// Visible codepoint index after which to insert, or -1 for before the first. + pub insert_after_index: i64, + /// Invisible marker characters to insert. + pub marker: String, +} + +/// A complete marker embedding plan. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EmbeddingPlan { + /// Index unit, which must be `codepoint`. + pub index_unit: String, + /// Ordered marker insertion operations. + pub operations: Vec, +} + +/// Parsed response from the Encypher CDN signing endpoint. +#[derive(Debug, Deserialize)] +pub struct SignResponse { + /// Whether signing succeeded. + pub success: bool, + /// Marker plan when signing succeeded. + #[serde(default)] + pub embedding_plan: Option, + /// C2PA manifest URL when signing succeeded. + #[serde(default)] + pub manifest_url: Option, + /// Stable failure reason when signing did not succeed. + #[serde(default)] + pub error: Option, + /// Actual policy used by the server; required before a successful plan is used. + #[serde(default)] + pub policy_digest: Option, +} + +/// Fail-closed signing-response parse errors. +#[derive(Debug, PartialEq)] +pub enum ApiError { + /// The response exceeded [`MAX_RESPONSE_BYTES`]. + TooLarge, + /// The response was not valid JSON for [`SignResponse`]. + Malformed, +} + +/// Parses a bounded signing response. +/// +/// # Errors +/// +/// Returns [`ApiError::TooLarge`] above the response limit and +/// [`ApiError::Malformed`] when JSON decoding fails. +pub fn parse_sign_response(body: &[u8]) -> Result { + if body.len() > MAX_RESPONSE_BYTES { + return Err(ApiError::TooLarge); + } + serde_json::from_slice(body).map_err(|_| ApiError::Malformed) +} + +/// A policy digest is exactly a lowercase SHA-256 hex value. +pub fn valid_policy_digest(digest: &str) -> bool { + digest.len() == 64 + && digest + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +/// Parse the policy endpoint's strict, bounded response. +/// +/// # Errors +/// +/// Rejects oversized JSON, extra or duplicate fields, and invalid digests. +pub fn parse_policy_response(body: &[u8]) -> Result { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct PolicyResponse { + policy_digest: String, + } + if body.len() > MAX_POLICY_RESPONSE_BYTES { + return Err(ApiError::TooLarge); + } + let response: PolicyResponse = serde_json::from_slice(body).map_err(|_| ApiError::Malformed)?; + if !valid_policy_digest(&response.policy_digest) { + return Err(ApiError::Malformed); + } + Ok(response.policy_digest) +} + +/// Reasons an embedding plan is unsafe or cannot address the visible text. +#[derive(Debug, PartialEq)] +pub enum PlanError { + /// The plan does not use Unicode codepoint indices. + IndexUnit, + /// An insertion index is outside the visible-text range. + IndexOutOfRange, + /// A marker is empty. + EmptyMarker, + /// A marker contains a character outside the edge-core invisible set. + DisallowedMarkerChar, + /// The plan has no operations. + Empty, + /// The manifest URL is present but not an absolute `https` URL. + ManifestUrl, +} + +/// Validates a signing result before use: index bounds and the invisible marker +/// alphabet for every operation, and a manifest URL that is absent or `https`. +/// +/// This is the only check between the API or KV and the page, so callers must +/// pass the scalar count of the exact text that was signed. +/// +/// # Errors +/// +/// Returns a [`PlanError`] for the first invalid property. +pub fn validate_plan( + plan: &EmbeddingPlan, + manifest_url: Option<&str>, + visible_scalars: usize, +) -> Result<(), PlanError> { + if plan.index_unit != "codepoint" { + return Err(PlanError::IndexUnit); + } + if plan.operations.is_empty() { + return Err(PlanError::Empty); + } + for operation in &plan.operations { + if operation.insert_after_index < -1 { + return Err(PlanError::IndexOutOfRange); + } + if operation.insert_after_index >= 0 { + let Ok(index) = usize::try_from(operation.insert_after_index) else { + return Err(PlanError::IndexOutOfRange); + }; + if index >= visible_scalars { + return Err(PlanError::IndexOutOfRange); + } + } + if operation.marker.is_empty() { + return Err(PlanError::EmptyMarker); + } + if !operation.marker.chars().all(is_allowed_marker_char) { + return Err(PlanError::DisallowedMarkerChar); + } + } + if let Some(url) = manifest_url { + let https = url::Url::parse(url).is_ok_and(|url| { + url.scheme() == "https" && url.host_str().is_some_and(|h| !h.is_empty()) + }); + if !https { + return Err(PlanError::ManifestUrl); + } + } + Ok(()) +} + +fn is_allowed_marker_char(ch: char) -> bool { + matches!(ch, '\u{fe00}'..='\u{fe0f}' | '\u{e0100}'..='\u{e01ef}' | '\u{feff}') +} + +#[cfg(test)] +mod tests { + use super::*; + + fn single_op_plan() -> EmbeddingPlan { + EmbeddingPlan { + index_unit: "codepoint".to_string(), + operations: vec![PlanOperation { + insert_after_index: 0, + marker: "\u{fe00}".to_string(), + }], + } + } + + #[test] + fn policy_response_requires_only_a_lowercase_sha256_digest() { + let digest = "ab".repeat(32); + let body = format!(r#"{{"policy_digest":"{digest}"}}"#); + assert_eq!(parse_policy_response(body.as_bytes()), Ok(digest.clone())); + for body in [ + "{}".to_owned(), + r#"{"policy_digest":null}"#.to_owned(), + r#"{"policy_digest":42}"#.to_owned(), + format!(r#"{{"policy_digest":"{}"}}"#, digest.to_uppercase()), + format!(r#"{{"policy_digest":"{}"}}"#, "g".repeat(64)), + format!(r#"{{"policy_digest":"{}"}}"#, "a".repeat(63)), + format!(r#"{{"policy_digest":"{digest}","extra":true}}"#), + format!(r#"{{"policy_digest":"{digest}","policy_digest":"{digest}"}}"#), + format!(r#"{{"policy_digest":"{digest}"}} trailing"#), + ] { + assert_eq!( + parse_policy_response(body.as_bytes()), + Err(ApiError::Malformed) + ); + } + assert_eq!( + parse_policy_response(&vec![b' '; MAX_POLICY_RESPONSE_BYTES + 1]), + Err(ApiError::TooLarge) + ); + } + + #[test] + fn parses_and_validates_a_response_plan() { + let body = r#"{"success":true,"embedding_plan":{"index_unit":"codepoint","operations":[{"insert_after_index":0,"marker":"︀"}]},"manifest_url":"https://manifest.example.com/x"}"#; + let response = parse_sign_response(body.as_bytes()).expect("should parse"); + let plan = response.embedding_plan.expect("should contain plan"); + assert_eq!( + validate_plan(&plan, response.manifest_url.as_deref(), 1), + Ok(()) + ); + } + + #[test] + fn manifest_url_must_be_absent_or_https() { + let plan = single_op_plan(); + assert_eq!(validate_plan(&plan, None, 1), Ok(())); + for url in [ + "", + "http://manifest.example.com/x", + "javascript:alert(1)", + "data:text/html,", + "/api/v1/public/cdn/manifest/1", + "https://", + ] { + assert_eq!( + validate_plan(&plan, Some(url), 1), + Err(PlanError::ManifestUrl), + "{url} should be rejected" + ); + } + } + + #[test] + fn rejects_markup_and_bad_ranges() { + let markup = EmbeddingPlan { + index_unit: "codepoint".to_string(), + operations: vec![PlanOperation { + insert_after_index: 0, + marker: "".to_string(), + }], + }; + assert_eq!( + validate_plan(&markup, None, 1), + Err(PlanError::DisallowedMarkerChar) + ); + assert_eq!( + validate_plan(&single_op_plan(), None, 0), + Err(PlanError::IndexOutOfRange) + ); + } + + #[test] + fn parses_realistic_large_marker_response() { + let escaped_marker = r"\uDB40\uDD00".repeat(50_000); + let body = format!( + r#"{{"success":true,"embedding_plan":{{"index_unit":"codepoint","operations":[{{"insert_after_index":0,"marker":"{escaped_marker}"}}]}}}}"# + ); + assert!(body.len() > 600_000); + let response = + parse_sign_response(body.as_bytes()).expect("large valid response should parse"); + let plan = response + .embedding_plan + .expect("response should contain a plan"); + assert_eq!(plan.operations[0].marker.chars().count(), 50_000); + } + + #[test] + fn enforces_size_and_empty_plan_limits() { + assert_eq!( + parse_sign_response(&vec![b' '; MAX_RESPONSE_BYTES + 1]).map(|_| ()), + Err(ApiError::TooLarge) + ); + let empty = EmbeddingPlan { + index_unit: "codepoint".to_string(), + operations: Vec::new(), + }; + assert_eq!(validate_plan(&empty, None, 1), Err(PlanError::Empty)); + } +} diff --git a/crates/provenance/encypher/src/boundary.rs b/crates/provenance/encypher/src/boundary.rs new file mode 100644 index 0000000000..e4aa722ece --- /dev/null +++ b/crates/provenance/encypher/src/boundary.rs @@ -0,0 +1,464 @@ +use super::text::{StructuralTag, TagKind, scan_structural_tags}; + +const CMS_CONTENT_CLASSES: [&str; 13] = [ + "entry-content", + "wp-block-post-content", + "post-content", + "article-body", + "article-content", + "story-body", + "story-body-text", + "gh-content", + "w-richtext", + "sqs-block-content", + "body markup", + "e-content", + "RichTextStoryBody", +]; + +/// The UTF-8 byte range and detection label of an article's outer HTML. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Boundary { + /// Inclusive byte offset of the opening element. + pub start: usize, + /// Exclusive byte offset after the first matching closing element. + pub end: usize, + /// Detection-chain selector label. + pub selector: String, +} + +#[derive(Debug, Clone, Copy)] +struct ElementMatch { + start: usize, + end: usize, +} + +#[derive(Clone, Copy)] +enum FirstMatch<'a> { + Unseen, + Open { start: usize, name: &'a str }, + Complete(ElementMatch), +} + +impl<'a> FirstMatch<'a> { + fn observe(&mut self, tag: StructuralTag<'a>, opening_matches: bool) { + match (*self, tag.kind) { + (Self::Unseen, TagKind::Opening) if opening_matches => { + *self = Self::Open { + start: tag.start, + name: tag.name, + }; + } + (Self::Open { start, name }, TagKind::Closing) + if tag.name.eq_ignore_ascii_case(name) => + { + *self = Self::Complete(ElementMatch { + start, + end: tag.end, + }); + } + _ => {} + } + } + + fn complete(self) -> Option { + match self { + Self::Complete(element) => Some(element), + Self::Unseen | Self::Open { .. } => None, + } + } +} + +#[derive(Clone, Copy)] +struct ActiveCount { + start: usize, + paragraphs: usize, +} + +#[derive(Clone, Copy)] +struct ScoredMatch { + element: ElementMatch, + paragraphs: usize, +} + +#[derive(Clone, Copy)] +struct CountedMatches { + name: &'static str, + active: Option, + best: Option, +} + +impl CountedMatches { + const fn new(name: &'static str) -> Self { + Self { + name, + active: None, + best: None, + } + } + + fn observe(&mut self, tag: StructuralTag<'_>) { + match tag.kind { + TagKind::Opening => { + if let Some(active) = &mut self.active { + if tag.name.eq_ignore_ascii_case("p") { + active.paragraphs += 1; + } + } else if tag.name.eq_ignore_ascii_case(self.name) { + self.active = Some(ActiveCount { + start: tag.start, + paragraphs: 0, + }); + } + } + TagKind::Closing if tag.name.eq_ignore_ascii_case(self.name) => { + let Some(active) = self.active.take() else { + return; + }; + let candidate = ScoredMatch { + element: ElementMatch { + start: active.start, + end: tag.end, + }, + paragraphs: active.paragraphs, + }; + if self + .best + .is_none_or(|best| candidate.paragraphs > best.paragraphs) + { + self.best = Some(candidate); + } + } + TagKind::Closing => {} + } + } +} + +enum Selector<'a> { + Tag(&'a str), + Class(&'a str), + Attribute(&'a str, &'a str), +} + +impl Selector<'_> { + fn matches(&self, tag: &StructuralTag<'_>) -> bool { + match *self { + Self::Tag(name) => tag.name.eq_ignore_ascii_case(name), + Self::Class(name) => tag + .attribute("class") + .is_some_and(|value| class_matches(value, name)), + Self::Attribute(attribute, value) => attribute_matches(tag, attribute, value), + } + } +} + +/// Finds an article boundary using the edge-core priority chain, excluding JSON-LD P5. +#[must_use] +pub fn find_article_boundary(html: &str, override_selector: Option<&str>) -> Option { + let override_label = override_selector.filter(|selector| !selector.is_empty()); + let override_matcher = override_label.and_then(parse_selector); + let mut override_element = FirstMatch::Unseen; + let mut article = CountedMatches::new("article"); + let mut cms = [FirstMatch::Unseen; CMS_CONTENT_CLASSES.len()]; + let mut itemprop = FirstMatch::Unseen; + let mut main = FirstMatch::Unseen; + let mut role = FirstMatch::Unseen; + let mut body = FirstMatch::Unseen; + let mut div_cluster = CountedMatches::new("div"); + let mut section_cluster = CountedMatches::new("section"); + let mut main_cluster = CountedMatches::new("main"); + + // All priority candidates share one structural pass. Each first-match + // state deliberately keeps an unclosed first opening from being rescued + // by a later one, matching the previous selector scans. + scan_structural_tags(html, |tag| { + let opening = tag.kind == TagKind::Opening; + override_element.observe( + tag, + opening + && override_matcher + .as_ref() + .is_some_and(|selector| selector.matches(&tag)), + ); + let class = opening.then(|| tag.attribute("class")).flatten(); + for (state, expected) in cms.iter_mut().zip(CMS_CONTENT_CLASSES) { + state.observe( + tag, + class.is_some_and(|value| class_matches(value, expected)), + ); + } + itemprop.observe( + tag, + opening && attribute_matches(&tag, "itemprop", "articleBody"), + ); + main.observe(tag, opening && tag.name.eq_ignore_ascii_case("main")); + role.observe(tag, opening && attribute_matches(&tag, "role", "main")); + body.observe(tag, opening && tag.name.eq_ignore_ascii_case("body")); + article.observe(tag); + div_cluster.observe(tag); + section_cluster.observe(tag); + main_cluster.observe(tag); + true + }); + + if let (Some(selector), Some(element)) = (override_label, override_element.complete()) { + return Some(boundary(element, format!("override:{selector}"))); + } + if let Some(selected) = article.best { + return Some(boundary(selected.element, "article")); + } + for (state, class_name) in cms.into_iter().zip(CMS_CONTENT_CLASSES) { + if let Some(element) = state.complete() { + return Some(boundary(element, format!("class:{class_name}"))); + } + } + if let Some(element) = itemprop.complete() { + return Some(boundary(element, "itemprop:articleBody")); + } + if let Some(element) = main.complete() { + return Some(boundary(element, "main")); + } + if let Some(element) = role.complete() { + return Some(boundary(element, "role:main")); + } + + let mut cluster = None; + for candidate in [div_cluster.best, section_cluster.best, main_cluster.best] + .into_iter() + .flatten() + .filter(|candidate| candidate.paragraphs > 4) + { + if cluster.is_none_or(|best: ScoredMatch| candidate.paragraphs > best.paragraphs) { + cluster = Some(candidate); + } + } + if let Some(selected) = cluster { + return Some(boundary(selected.element, "p-cluster")); + } + body.complete().map(|element| boundary(element, "body")) +} + +fn boundary(element: ElementMatch, selector: impl Into) -> Boundary { + Boundary { + start: element.start, + end: element.end, + selector: selector.into(), + } +} + +/// Checks the same restricted selector grammar used for article matching. +pub(super) fn is_valid_selector(selector: &str) -> bool { + parse_selector(selector).is_some() +} + +fn parse_selector(selector: &str) -> Option> { + if let Some(class_name) = selector.strip_prefix('.') { + return is_class_identifier(class_name).then_some(Selector::Class(class_name)); + } + if selector.starts_with('[') { + let (attribute, value) = parse_attribute_selector(selector)?; + return Some(Selector::Attribute(attribute, value)); + } + (selector + .as_bytes() + .first() + .is_some_and(u8::is_ascii_alphabetic) + && selector + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))) + .then_some(Selector::Tag(selector)) +} + +fn is_identifier_character(character: char) -> bool { + character.is_ascii_alphanumeric() || matches!(character, '_' | '-') || character >= '\u{80}' +} + +fn is_class_identifier(name: &str) -> bool { + let mut characters = name.chars(); + let Some(mut first) = characters.next() else { + return false; + }; + if first == '-' { + let Some(next) = characters.next() else { + return false; + }; + first = next; + } + (first.is_ascii_alphabetic() || matches!(first, '_' | '-') || first >= '\u{80}') + && characters.all(is_identifier_character) +} + +fn parse_attribute_selector(selector: &str) -> Option<(&str, &str)> { + let content = selector.strip_prefix('[')?.strip_suffix(']')?; + let (attribute, raw_value) = content.split_once('=')?; + if attribute.is_empty() + || !attribute + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')) + { + return None; + } + let value = match ( + raw_value.as_bytes().first().copied(), + raw_value.as_bytes().last().copied(), + ) { + (Some(first @ (b'"' | b'\'')), Some(last)) if first == last => { + let value = raw_value.get(1..raw_value.len().saturating_sub(1))?; + return (!value.is_empty() + && !value.chars().any(|character| { + character.is_control() || matches!(character, '"' | '\'' | ']' | '\\') + })) + .then_some((attribute, value)); + } + (Some(b'"' | b'\''), _) => return None, + _ => raw_value, + }; + (!value.is_empty() && value.chars().all(is_identifier_character)).then_some((attribute, value)) +} + +fn attribute_matches(tag: &StructuralTag<'_>, attribute: &str, expected: &str) -> bool { + tag.attribute(attribute) + .is_some_and(|value| value.eq_ignore_ascii_case(expected)) +} + +fn class_matches(value: &str, expected: &str) -> bool { + let mut expected_parts = expected.split_ascii_whitespace(); + let Some(first) = expected_parts.next() else { + return false; + }; + let second = expected_parts.next(); + if expected_parts.next().is_some() { + return false; + } + let mut previous_was_first = false; + for class in value.split_ascii_whitespace() { + if second.is_some_and(|second| previous_was_first && class.eq_ignore_ascii_case(second)) { + return true; + } + previous_was_first = class.eq_ignore_ascii_case(first); + if second.is_none() && previous_was_first { + return true; + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn follows_priority_chain_and_byte_offsets() { + let html = "🐟

Main.

Chosen.

"; + let boundary = find_article_boundary(html, None).expect("should find article"); + assert_eq!(boundary.selector, "article"); + assert_eq!( + html.get(boundary.start..boundary.end), + Some("

Chosen.

") + ); + } + + #[test] + fn nested_same_name_uses_first_closing_tag() { + let html = "
outer
inner
tail
"; + let boundary = find_article_boundary(html, None).expect("should find article"); + assert_eq!( + html.get(boundary.start..boundary.end), + Some("
outer
inner
") + ); + } + + #[test] + fn closing_tags_match_ascii_case_insensitively() { + let html = "

One.

x
"; + let boundary = find_article_boundary(html, None).expect("should find article"); + assert_eq!( + html.get(boundary.start..boundary.end), + Some("

One.

") + ); + let boundary = + find_article_boundary(html, Some(".entry-content")).expect("should find override"); + assert_eq!( + html.get(boundary.start..boundary.end), + Some("
x
") + ); + } + + #[test] + fn article_detection_ignores_inert_opening_markup() { + let real = "

Real article.

"; + for inert in [ + "", + r#""#, + r#"
"#, + ] { + let html = format!("{inert}{real}"); + let boundary = find_article_boundary(&html, None).expect("should find real article"); + assert_eq!( + html.get(boundary.start..boundary.end), + Some(real), + "inert markup must not become the article: {inert}" + ); + } + } + + #[test] + fn inert_closing_markup_does_not_truncate_an_article() { + for inert in [ + "", + r#""#, + r#"Attribute text."#, + ] { + let html = format!("

Start.

{inert}

End.

"); + let boundary = find_article_boundary(&html, None).expect("should find article"); + assert_eq!( + html.get(boundary.start..boundary.end), + Some(html.as_str()), + "inert markup must not close the article: {inert}" + ); + } + } + + #[test] + fn invalid_less_than_and_declarations_do_not_hide_or_create_articles() { + let real = "

Real article.

"; + for prefix in [ + "text < y ", + "< article>

False spaced article.

", + "

False declaration.

", + "

False instruction.

", + " remainder\">", + ] { + let html = format!("{prefix}{real}"); + let boundary = find_article_boundary(&html, None).expect("should find real article"); + assert_eq!( + html.get(boundary.start..boundary.end), + Some(real), + "invalid markup must not hide or create the article: {prefix}" + ); + } + + let html = "

Start.

End.

"; + let boundary = find_article_boundary(html, None).expect("should find article"); + assert_eq!(html.get(boundary.start..boundary.end), Some(html)); + } + + #[test] + fn dense_documents_select_articles_without_a_document_sized_tag_index() { + let padding = "".repeat(700_000); + let first = "

First.

"; + let second = "

Second.

Wins.

"; + let html = format!("{padding}{first}{second}"); + + let boundary = find_article_boundary(&html, None).expect("should find article"); + assert_eq!(boundary.selector, "article"); + assert_eq!(html.get(boundary.start..boundary.end), Some(second)); + } + + #[test] + fn selects_largest_paragraph_cluster_at_five() { + let html = "

1

2

3

4

5

"; + let boundary = find_article_boundary(html, None).expect("should find cluster"); + assert_eq!(boundary.selector, "p-cluster"); + } +} diff --git a/crates/provenance/encypher/src/data_uri.rs b/crates/provenance/encypher/src/data_uri.rs new file mode 100644 index 0000000000..8edf13e054 --- /dev/null +++ b/crates/provenance/encypher/src/data_uri.rs @@ -0,0 +1,314 @@ +//! `data:` URIs carrying media: parsing, exact decoding and lossless +//! re-encoding in the URI's own encoding. +//! +//! A URI is rewritten as `data:` (as written), its header text between the +//! scheme and the comma (as written), and the signed bytes encoded the way +//! the original payload was: base64, padded only if the original was, or +//! percent-encoding with a set that is safe in an HTML attribute (quoted or +//! not), a `srcset` candidate and a CSS `url()`. Decoding the new URI gives +//! back exactly the signed bytes. + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}; + +use crate::media_api::canonical_media_type; + +/// Forgiving base64, as browsers decode a `data:` URI: padding optional and +/// trailing bits ignored. ASCII whitespace is removed before decoding. +const FORGIVING: GeneralPurpose = GeneralPurpose::new( + &base64::alphabet::STANDARD, + GeneralPurposeConfig::new() + .with_decode_padding_mode(DecodePaddingMode::Indifferent) + .with_decode_allow_trailing_bits(true), +); + +/// One `data:` URI whose media type is media the module signs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct DataUri<'a> { + /// The scheme as written, `data:` in any case. + scheme: &'a str, + /// Everything between the scheme and the comma, as written. + header: &'a str, + /// The payload after the comma. + payload: &'a str, + base64: bool, +} + +/// Why a media `data:` URI cannot be decoded. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Malformed; + +/// Whether `media_type` is media: an image, audio, video or font. +pub(crate) fn is_media_type(media_type: &str) -> bool { + ["image/", "audio/", "video/", "font/"] + .iter() + .any(|prefix| media_type.starts_with(prefix) && media_type.len() > prefix.len()) +} + +impl<'a> DataUri<'a> { + /// The `data:` URI `value` is, when it declares media. Any other value, + /// or a `data:` URI of another type, is `None`. + pub(crate) fn parse(value: &'a str) -> Option { + let scheme = value.get(..5)?; + if !scheme.eq_ignore_ascii_case("data:") { + return None; + } + let rest = value.get(5..)?; + let comma = rest.find(',')?; + let header = rest.get(..comma)?; + let payload = rest.get(comma + 1..)?; + let uri = Self { + scheme, + header, + payload, + base64: header + .rsplit(';') + .next() + .is_some_and(|last| last.trim().eq_ignore_ascii_case("base64")) + && header.contains(';'), + }; + is_media_type(&uri.media_type()).then_some(uri) + } + + /// The bare, lower-case media type the URI declares. + pub(crate) fn media_type(&self) -> String { + canonical_media_type(self.header.split(';').next().unwrap_or_default().trim()) + } + + /// The declared type with its parameters, as written, without `;base64`, + /// sent as the source's type hint. + pub(crate) fn declared_type(&self) -> &'a str { + if self.base64 { + self.header + .rsplit_once(';') + .map_or(self.header, |(declared, _)| declared) + } else { + self.header + } + } + + /// The exact bytes the URI carries. + /// + /// # Errors + /// + /// Returns [`Malformed`] for base64 a browser would refuse. + pub(crate) fn decode(&self) -> Result, Malformed> { + if self.base64 { + let compact: Vec = self + .payload + .bytes() + .filter(|byte| !byte.is_ascii_whitespace()) + .collect(); + return FORGIVING.decode(compact).map_err(|_| Malformed); + } + Ok(percent_decode(self.payload.as_bytes())) + } + + /// The URI for `bytes`, with this URI's scheme and header text, encoded + /// as this URI's payload was. + pub(crate) fn encode(&self, bytes: &[u8]) -> String { + let mut out = String::with_capacity( + self.scheme.len() + self.header.len() + 1 + bytes.len().saturating_mul(3), + ); + out.push_str(self.scheme); + out.push_str(self.header); + out.push(','); + if self.base64 { + let encoded = STANDARD.encode(bytes); + if self.payload.trim_end().ends_with('=') || self.unpadded_length_is_aligned() { + out.push_str(&encoded); + } else { + out.push_str(encoded.trim_end_matches('=')); + } + } else { + percent_encode_into(&mut out, bytes); + } + out + } + + /// Whether the original payload needed no padding, so writing padding + /// keeps its form. + fn unpadded_length_is_aligned(&self) -> bool { + self.payload + .bytes() + .filter(|byte| !byte.is_ascii_whitespace()) + .count() + % 4 + == 0 + } +} + +/// Decodes `%XX` escapes. A `%` without two hex digits stays as written, as +/// browsers read a `data:` URI. +fn percent_decode(input: &[u8]) -> Vec { + let mut out = Vec::with_capacity(input.len()); + let mut index = 0; + while let Some(&byte) = input.get(index) { + if byte == b'%' + && let (Some(high), Some(low)) = ( + input.get(index + 1).and_then(|digit| hex_value(*digit)), + input.get(index + 2).and_then(|digit| hex_value(*digit)), + ) + { + out.push((high << 4) | low); + index += 3; + continue; + } + out.push(byte); + index += 1; + } + out +} + +fn hex_value(digit: u8) -> Option { + match digit { + b'0'..=b'9' => Some(digit - b'0'), + b'a'..=b'f' => Some(digit - b'a' + 10), + b'A'..=b'F' => Some(digit - b'A' + 10), + _ => None, + } +} + +/// Whether `byte` is written as itself in a percent-encoded payload. Quotes, +/// `&`, `<`, `>`, `=`, whitespace, parentheses, `\`, `,`, `#` and `%` are +/// always escaped, so the payload is safe in every context the module +/// rewrites and a `srcset` candidate never ends in a comma. +fn is_literal(byte: u8) -> bool { + byte.is_ascii_alphanumeric() + || matches!( + byte, + b'-' | b'.' + | b'_' + | b'~' + | b'!' + | b'$' + | b'*' + | b'+' + | b';' + | b':' + | b'@' + | b'/' + | b'?' + ) +} + +fn percent_encode_into(out: &mut String, bytes: &[u8]) { + const DIGITS: &[u8; 16] = b"0123456789ABCDEF"; + for &byte in bytes { + if is_literal(byte) { + out.push(char::from(byte)); + } else { + out.push('%'); + out.push(char::from(DIGITS[usize::from(byte >> 4)])); + out.push(char::from(DIGITS[usize::from(byte & 0x0f)])); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_media_data_uris_are_parsed() { + assert!(DataUri::parse("data:image/png;base64,iVBORw0KGgo=").is_some()); + assert!(DataUri::parse("DATA:Image/SVG+xml;charset=utf-8,%3Csvg%3E").is_some()); + assert!(DataUri::parse("data:font/woff2;base64,d09GMg==").is_some()); + for value in [ + "data:text/html,

x

", + "data:,plain", + "data:application/json;base64,e30=", + "https://cdn.example.com/a.png", + "data:image/png;base64", + ] { + assert_eq!(DataUri::parse(value), None, "should not sign {value}"); + } + } + + #[test] + fn base64_round_trips_exact_bytes_and_keeps_header_and_padding_style() { + let uri = DataUri::parse("Data:image/png;name=a.png;BASE64,AAEC/w==") + .expect("should parse a media data URI"); + assert_eq!(uri.media_type(), "image/png"); + assert_eq!(uri.declared_type(), "image/png;name=a.png"); + assert_eq!( + uri.decode().expect("should decode"), + vec![0, 1, 2, 255], + "should decode the exact bytes" + ); + let signed: Vec = (0..=255).collect(); + let encoded = uri.encode(&signed); + assert!( + encoded.starts_with("Data:image/png;name=a.png;BASE64,"), + "should keep the scheme and header as written: {encoded}" + ); + let reparsed = DataUri::parse(&encoded).expect("should reparse"); + assert_eq!( + reparsed.decode().expect("should decode"), + signed, + "should round-trip" + ); + + let unpadded = DataUri::parse("data:image/gif;base64,AAEC/w").expect("should parse"); + let encoded = unpadded.encode(&[1, 2]); + assert!(!encoded.ends_with('='), "should keep an unpadded form"); + assert_eq!( + DataUri::parse(&encoded) + .expect("should reparse") + .decode() + .expect("should decode"), + vec![1, 2] + ); + } + + #[test] + fn wrapped_base64_is_decoded_as_browsers_do() { + let uri = DataUri::parse("data:image/png;base64,AAEC\n /w==").expect("should parse"); + assert_eq!(uri.decode().expect("should decode"), vec![0, 1, 2, 255]); + let bad = DataUri::parse("data:image/png;base64,A*==").expect("should parse"); + assert_eq!(bad.decode(), Err(Malformed), "should refuse invalid base64"); + } + + #[test] + fn percent_encoded_svg_round_trips_and_is_context_safe() { + let original = "data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E"; + let uri = DataUri::parse(original).expect("should parse"); + let decoded = uri.decode().expect("should decode"); + assert_eq!( + decoded, + b"".to_vec(), + "should decode escapes and keep literal bytes" + ); + let signed = b"(x), #&\\ \n100%\xff"; + let encoded = uri.encode(signed); + assert!( + !encoded[original.find(',').expect("comma") + 1..] + .bytes() + .any(|byte| matches!( + byte, + b'"' | b'\'' + | b'<' + | b'>' + | b'&' + | b'=' + | b'(' + | b')' + | b'\\' + | b',' + | b'#' + | b' ' + | b'\n' + )), + "should escape every context-sensitive byte: {encoded}" + ); + assert_eq!( + DataUri::parse(&encoded) + .expect("should reparse") + .decode() + .expect("should decode"), + signed.to_vec(), + "should round-trip exactly" + ); + } +} diff --git a/crates/provenance/encypher/src/embed.rs b/crates/provenance/encypher/src/embed.rs new file mode 100644 index 0000000000..58b811d8ad --- /dev/null +++ b/crates/provenance/encypher/src/embed.rs @@ -0,0 +1,523 @@ +use super::api::EmbeddingPlan; +use super::boundary::Boundary; +use super::text::visit_canonical_text; + +const MAGIC_PREFIX: &str = "\u{e01df}\u{e01e0}\u{e01e1}\u{e01e2}"; +const MANIFEST_META_PREFIX: &str = r#""#; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ApplyError { + Alignment, + TooLarge, +} + +/// Reports whether a string contains the four-character Encypher magic prefix. +#[must_use] +pub fn has_existing_markers(s: &str) -> bool { + s.contains(MAGIC_PREFIX) +} + +/// Applies a codepoint embedding plan and optional manifest insertion in one +/// exact-capacity output allocation. +/// +/// The plan must already have passed [`super::api::validate_plan`]. At an +/// equal source offset, plan markers precede the manifest in plan order. +pub(crate) fn apply_plan( + html: &str, + boundary: &Boundary, + plan: &EmbeddingPlan, + manifest: Option<(&str, usize)>, + max_output_bytes: usize, +) -> Result { + let marker_bytes = plan + .operations + .iter() + .try_fold(0usize, |total, operation| { + total.checked_add(operation.marker.len()) + }) + .ok_or(ApplyError::TooLarge)?; + let manifest_bytes = manifest + .map_or(Some(0), |(url, _)| manifest_meta_len(url)) + .ok_or(ApplyError::TooLarge)?; + let output_len = html + .len() + .checked_add(marker_bytes) + .and_then(|with_markers| with_markers.checked_add(manifest_bytes)) + .ok_or(ApplyError::TooLarge)?; + if output_len > max_output_bytes { + return Err(ApplyError::TooLarge); + } + + let boundary_html = html + .get(boundary.start..boundary.end) + .ok_or(ApplyError::Alignment)?; + let offsets_needed = match plan + .operations + .iter() + .filter_map(|operation| usize::try_from(operation.insert_after_index).ok()) + .max() + { + Some(index) => index.checked_add(1).ok_or(ApplyError::Alignment)?, + None => 0, + }; + let mut insertion_offsets = Vec::with_capacity(offsets_needed.min(boundary_html.len())); + let mut first_visible_start = None; + visit_canonical_text(boundary_html, |item| { + if first_visible_start.is_none() { + first_visible_start = Some(item.start); + } + if insertion_offsets.len() < offsets_needed { + insertion_offsets.push(item.end); + } + insertion_offsets.len() < offsets_needed + }); + let first_visible_start = first_visible_start.ok_or(ApplyError::Alignment)?; + + let mut insertions = Vec::with_capacity(plan.operations.len()); + for operation in &plan.operations { + let relative = if operation.insert_after_index == -1 { + first_visible_start + } else { + let index = + usize::try_from(operation.insert_after_index).map_err(|_| ApplyError::Alignment)?; + insertion_offsets + .get(index) + .copied() + .ok_or(ApplyError::Alignment)? + }; + let absolute = boundary + .start + .checked_add(relative) + .ok_or(ApplyError::Alignment)?; + insertions.push((absolute, operation.marker.as_str())); + } + // Stable sorting preserves API order for multiple markers at the same offset. + insertions.sort_by_key(|(offset, _)| *offset); + + let mut result = String::with_capacity(output_len); + let mut cursor = 0; + let mut manifest = manifest; + for (offset, marker) in insertions { + if manifest.is_some_and(|(_, manifest_offset)| manifest_offset < offset) { + let (url, manifest_offset) = manifest.take().ok_or(ApplyError::Alignment)?; + result.push_str( + html.get(cursor..manifest_offset) + .ok_or(ApplyError::Alignment)?, + ); + push_manifest_meta(&mut result, url); + cursor = manifest_offset; + } + result.push_str(html.get(cursor..offset).ok_or(ApplyError::Alignment)?); + result.push_str(marker); + cursor = offset; + } + if let Some((url, manifest_offset)) = manifest { + result.push_str( + html.get(cursor..manifest_offset) + .ok_or(ApplyError::Alignment)?, + ); + push_manifest_meta(&mut result, url); + cursor = manifest_offset; + } + result.push_str(html.get(cursor..).ok_or(ApplyError::Alignment)?); + Ok(result) +} + +/// Exact byte length of a manifest meta element after attribute escaping. +fn manifest_meta_len(manifest_url: &str) -> Option { + manifest_url.chars().try_fold( + MANIFEST_META_PREFIX.len() + MANIFEST_META_SUFFIX.len(), + |total, character| { + let bytes = match character { + '&' => 5, + '"' => 6, + '<' | '>' => 4, + _ => character.len_utf8(), + }; + total.checked_add(bytes) + }, + ) +} + +fn push_manifest_meta(output: &mut String, manifest_url: &str) { + output.push_str(MANIFEST_META_PREFIX); + for character in manifest_url.chars() { + match character { + '&' => output.push_str("&"), + '"' => output.push_str("""), + '<' => output.push_str("<"), + '>' => output.push_str(">"), + _ => output.push(character), + } + } + output.push_str(MANIFEST_META_SUFFIX); +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + use crate::api::{EmbeddingPlan, PlanOperation}; + use crate::boundary::{Boundary, find_article_boundary}; + use crate::text::{document_metadata, visible_text}; + + fn apply(html: &str, boundary: &Boundary, plan: &EmbeddingPlan) -> Option { + apply_plan(html, boundary, plan, None, usize::MAX).ok() + } + + fn manifest_output(html: &str, manifest_url: &str) -> String { + let metadata = document_metadata(html); + if manifest_url.is_empty() || metadata.manifest_present { + return html.to_owned(); + } + let Some(offset) = metadata.head_close else { + return html.to_owned(); + }; + let mut output = + String::with_capacity(html.len() + manifest_meta_len(manifest_url).unwrap_or(0)); + output.push_str(html.get(..offset).expect("metadata offset should be valid")); + push_manifest_meta(&mut output, manifest_url); + output.push_str(html.get(offset..).expect("metadata offset should be valid")); + output + } + + #[test] + fn adversarial_ampersands_have_bounded_embedding_work() { + let html = format!("
{}
", "&".repeat(100_000)); + let boundary = find_article_boundary(&html, None).expect("article"); + let plan = EmbeddingPlan { + index_unit: "codepoint".to_owned(), + operations: vec![PlanOperation { + insert_after_index: 0, + marker: "\u{fe00}".to_owned(), + }], + }; + let output = apply(&html, &boundary, &plan).expect("valid plan"); + assert_eq!( + output, + format!("
&\u{fe00}{}
", "&".repeat(99_999)) + ); + } + + #[test] + fn ordered_entity_replacements_preserve_original_insertion_spans() { + let html = "
&lt;|&#65;|&lt;
"; + let boundary = find_article_boundary(html, None).expect("article"); + let plan = EmbeddingPlan { + index_unit: "codepoint".to_owned(), + operations: vec![ + PlanOperation { + insert_after_index: 0, + marker: "\u{fe00}".to_owned(), + }, + PlanOperation { + insert_after_index: 2, + marker: "\u{fe01}".to_owned(), + }, + PlanOperation { + insert_after_index: 4, + marker: "\u{fe02}".to_owned(), + }, + ], + }; + + assert_eq!( + visible_text( + html.get(boundary.start..boundary.end) + .expect("boundary should be valid"), + usize::MAX, + ) + .0, + "<|A|<" + ); + assert_eq!( + apply(html, &boundary, &plan), + Some( + "
&lt;\u{fe00}|&#65;\u{fe01}|&\u{fe02}lt;
".to_owned() + ) + ); + } + + #[test] + fn dense_plan_on_large_document_preserves_operation_order() { + let html = format!( + "
{}
", + "a".repeat(50_000), + "x".repeat(4_900_000) + ); + let boundary = find_article_boundary(&html, None).expect("article"); + let plan = EmbeddingPlan { + index_unit: "codepoint".to_owned(), + operations: (0..20_000) + .rev() + .map(|index| PlanOperation { + insert_after_index: index, + marker: "\u{fe00}".to_owned(), + }) + .collect(), + }; + let output = apply(&html, &boundary, &plan).expect("valid dense plan"); + assert_eq!( + output, + format!( + "
{}{}
", + "a\u{fe00}".repeat(20_000), + "a".repeat(30_000), + "x".repeat(4_900_000) + ) + ); + } + + #[test] + fn sparse_plan_on_markup_dense_article_bounds_fragment_work() { + let html = format!("
{}
", "a".repeat(612_500)); + let boundary = find_article_boundary(&html, None).expect("article"); + let plan = EmbeddingPlan { + index_unit: "codepoint".to_owned(), + operations: vec![ + PlanOperation { + insert_after_index: -1, + marker: "\u{fe00}".to_owned(), + }, + PlanOperation { + insert_after_index: 0, + marker: "\u{fe01}".to_owned(), + }, + PlanOperation { + insert_after_index: 0, + marker: "\u{fe02}".to_owned(), + }, + ], + }; + + let output = apply(&html, &boundary, &plan).expect("valid sparse plan"); + let marker_bytes = plan + .operations + .iter() + .map(|operation| operation.marker.len()) + .sum::(); + assert_eq!(output.len(), html.len() + marker_bytes); + assert!(output.starts_with("
\u{fe00}a\u{fe01}\u{fe02}")); + assert!(output.ends_with("
")); + } + + #[derive(Deserialize)] + struct FixtureFile { + cases: Vec, + } + + #[derive(Deserialize)] + struct FixtureCase { + id: String, + html: String, + override_selector: Option, + boundary: FixtureBoundary, + text: String, + text_rust: String, + text_divergence: Option, + plan: EmbeddingPlan, + expected_output: String, + invalid_plan: Option, + expected_invalid_output: Option, + manifest_url: String, + expected_meta_output_js: String, + expected_meta_output_rust: String, + meta_divergence: Option, + } + + #[derive(Deserialize)] + struct FixtureBoundary { + selector: String, + start: usize, + end: usize, + } + + fn fixtures() -> FixtureFile { + serde_json::from_str(include_str!("fixtures/parity.json")) + .expect("parity fixtures should deserialize") + } + + #[test] + fn groups_same_offset_in_plan_order() { + let html = "🐟
A B
"; + let start = html.find("
").expect("should find boundary"); + let boundary = Boundary { + start, + end: html.len(), + selector: "article".to_string(), + }; + let plan = EmbeddingPlan { + index_unit: "codepoint".to_string(), + operations: vec![ + PlanOperation { + insert_after_index: 0, + marker: "\u{fe00}".to_string(), + }, + PlanOperation { + insert_after_index: 0, + marker: "\u{fe01}".to_string(), + }, + ], + }; + assert_eq!( + apply(html, &boundary, &plan), + Some("🐟
A\u{fe00}\u{fe01} B
".to_string()) + ); + } + + #[test] + fn manifest_meta_uses_structural_head_close_and_escapes() { + let html = ""; + let output = manifest_output(html, "https://example.com/?a=1&b=\"x\""); + assert!(output.contains(" -->"#, + r#""#, + r#"
"#, + ] { + assert!( + !document_metadata(html).manifest_present, + "inert markup must not declare a manifest: {html}" + ); + } + assert!( + document_metadata( + r#""# + ) + .manifest_present + ); + } + + #[test] + fn invalid_less_than_and_declarations_do_not_hide_manifest_meta() { + let html = concat!( + "text < y ", + " remainder\">", + "", + r#""# + ); + assert!(document_metadata(html).manifest_present); + assert!( + !document_metadata( + r#"< meta name="c2pa-manifest-url">"# + ) + .manifest_present + ); + } + + #[test] + fn manifest_meta_does_not_activate_quoted_markup() { + let prefix = r#""#; + let html = format!("{prefix}
Visible
"); + assert_eq!( + manifest_output(&html, "javascript:alert(1)"), + format!( + "{prefix}
Visible
" + ) + ); + // The URL is inert metadata, not an href, script source or refresh target. + let script = + r#""#; + assert_eq!( + manifest_output( + &format!("{script}"), + "data:text/html," + ), + format!( + "{script}" + ) + ); + } + + #[test] + fn manifest_meta_ignores_quotes_inside_unquoted_values() { + // The browser reads `x"y` as an unquoted value, so the second meta's quoted + // content, including its ``, is attribute text. + let prefix = + r#""#; + let html = format!("{prefix}Visible"); + assert_eq!( + manifest_output(&html, "https://manifest.example.com/m"), + format!( + "{prefix}Visible" + ) + ); + } + + #[test] + fn edge_core_parity_fixtures_match() { + for fixture in fixtures().cases { + let boundary = + find_article_boundary(&fixture.html, fixture.override_selector.as_deref()) + .unwrap_or_else(|| panic!("{} should have a boundary", fixture.id)); + assert_eq!( + boundary.selector, fixture.boundary.selector, + "{}", + fixture.id + ); + assert_eq!(boundary.start, fixture.boundary.start, "{}", fixture.id); + assert_eq!(boundary.end, fixture.boundary.end, "{}", fixture.id); + let boundary_html = fixture + .html + .get(boundary.start..boundary.end) + .expect("fixture boundary should be valid"); + let text = visible_text(boundary_html, usize::MAX).0; + assert_eq!(text, fixture.text_rust, "{}", fixture.id); + if fixture.text_divergence.is_some() { + assert_ne!(text, fixture.text, "{}", fixture.id); + } else { + assert_eq!(text, fixture.text, "{}", fixture.id); + } + assert_eq!( + apply(&fixture.html, &boundary, &fixture.plan), + Some(fixture.expected_output), + "{}", + fixture.id + ); + if let Some(invalid_plan) = fixture.invalid_plan { + assert_eq!( + apply(&fixture.html, &boundary, &invalid_plan), + fixture.expected_invalid_output, + "{}", + fixture.id + ); + } + let meta = manifest_output(&fixture.html, &fixture.manifest_url); + assert_eq!(meta, fixture.expected_meta_output_rust, "{}", fixture.id); + if fixture.meta_divergence.is_some() { + assert_ne!(meta, fixture.expected_meta_output_js, "{}", fixture.id); + } else { + assert_eq!(meta, fixture.expected_meta_output_js, "{}", fixture.id); + } + } + } + + #[test] + fn every_fixture_byte_prefix_is_panic_free() { + for fixture in fixtures().cases { + for end in 0..=fixture.html.len() { + let prefix = String::from_utf8_lossy( + fixture + .html + .as_bytes() + .get(..end) + .expect("prefix range should be valid"), + ); + let _ = visible_text(&prefix, usize::MAX); + if let Some(boundary) = + find_article_boundary(&prefix, fixture.override_selector.as_deref()) + { + let _ = apply(&prefix, &boundary, &fixture.plan); + } + let _ = manifest_output(&prefix, &fixture.manifest_url); + } + } + } +} diff --git a/crates/provenance/encypher/src/embedded.rs b/crates/provenance/encypher/src/embedded.rs new file mode 100644 index 0000000000..b9f5bd4a89 --- /dev/null +++ b/crates/provenance/encypher/src/embedded.rs @@ -0,0 +1,1248 @@ +//! Media embedded in a document: `data:` URIs in HTML attributes, `srcset` +//! candidates, inline styles and stylesheets, and inline `` elements. +//! +//! Scanning is pure. It finds the exact byte range of each media `data:` +//! URI the module can rewrite without changing anything else, and the reason +//! for each one it cannot: an inline style a page security policy admits by +//! hash, or whose policies the module cannot see, a subresource integrity +//! value on the element, or an escape the module would have to decode. A +//! refused URI stays as written and is reported as unsigned. Inline SVG is +//! signed as the standalone SVG document +//! [`signable_svg`](crate::svg_equivalence::signable_svg) proves equivalent +//! to what the browser parsed, and only kept when the signer's writer added +//! its manifest carrier and changed nothing else. + +use crate::data_uri::DataUri; +use crate::processor::Reason; +use crate::text::{ + comment_end, find_byte, find_bytes, is_structural_element_start, raw_close_name_end, + starts_with_at, tag_end, take_tag_name, +}; + +/// Elements whose content the HTML tokenizer never reads as markup. Unlike +/// the text path, `noscript` content is scanned as markup, because readers +/// without scripts see the images inside it. +const RAW_TEXT: [&str; 9] = [ + "script", + "style", + "title", + "textarea", + "xmp", + "iframe", + "noembed", + "noframes", + "plaintext", +]; + +/// Where a found `data:` URI sits, which decides what may refuse its edit. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Site { + /// A URL attribute or a `srcset` candidate. + Attribute, + /// A `style` attribute, which a page policy may admit by hash. + StyleAttribute, + /// A `` elements. + meta_policies: Vec<&'a str>, + /// A `` policy written in a way the module does not read. + unreadable_meta_policy: bool, +} + +impl<'a> Collector<'a> { + fn new(document: &'a str) -> Self { + Self { + document, + scan: Scan::default(), + inline: Vec::new(), + meta_policies: Vec::new(), + unreadable_meta_policy: false, + } + } + + /// Keep the URI at `range` when it is media the module can rewrite in + /// place, or report why it is left as written. + fn consider(&mut self, (start, end): (usize, usize), site: Site, integrity: bool) { + let Some(text) = self.document.get(start..end) else { + return; + }; + if DataUri::parse(text).is_none() { + return; + } + let refusal = if integrity { + Some(Reason::IntegrityAttribute) + } else if text.contains('\\') && site != Site::Attribute { + Some(Reason::CssEscape) + } else if text.contains('&') + && matches!( + site, + Site::Attribute | Site::StyleAttribute | Site::StyleElement { foreign: true, .. } + ) + { + // A character reference the module would have to decode, and + // could not write back as it was. + Some(Reason::CharacterReference) + } else { + None + }; + let found = Found { start, end }; + match (refusal, site) { + (Some(reason), _) => self.scan.refused.push(reason), + (None, Site::StyleAttribute | Site::StyleElement { .. }) => { + self.inline.push((found, site)); + } + (None, _) => self.scan.found.push(found), + } + } + + /// Each `srcset` candidate URL in the value at `range`. + fn srcset(&mut self, (start, end): (usize, usize), integrity: bool) { + let bytes = self.document.as_bytes(); + let mut cursor = start; + while cursor < end { + while cursor < end + && bytes + .get(cursor) + .is_some_and(|byte| byte.is_ascii_whitespace() || *byte == b',') + { + cursor += 1; + } + let url_start = cursor; + while cursor < end + && bytes + .get(cursor) + .is_some_and(|byte| !byte.is_ascii_whitespace()) + { + cursor += 1; + } + let mut url_end = cursor; + while url_end > url_start && bytes.get(url_end - 1) == Some(&b',') { + url_end -= 1; + } + if url_end > url_start { + self.consider((url_start, url_end), Site::Attribute, integrity); + } + if url_end < cursor { + continue; + } + // The descriptors run to the next comma. + while cursor < end && bytes.get(cursor).is_some_and(|byte| *byte != b',') { + cursor += 1; + } + } + } + + /// One start tag's media URIs. + fn tag(&mut self, name: &str, token: &'a str, offset: usize) { + let attributes = attributes(token, offset); + let integrity = attributes + .iter() + .any(|attribute| attribute.name.eq_ignore_ascii_case("integrity")); + if name.eq_ignore_ascii_case("meta") { + self.meta_policy(&attributes); + } + for attribute in &attributes { + let Some(range) = attribute.value else { + continue; + }; + if attribute.name.eq_ignore_ascii_case("style") { + self.css(range, Site::StyleAttribute); + } else if attribute + .name + .get(attribute.name.len().saturating_sub(6)..) + .is_some_and(|suffix| suffix.eq_ignore_ascii_case("srcset")) + { + self.srcset(range, integrity); + } else if is_url_attribute(attribute.name) { + let range = trimmed(self.document, range); + let starts_data = self + .document + .get(range.0..range.0.saturating_add(5).min(range.1)) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case("data:")); + if starts_data { + self.consider(range, Site::Attribute, integrity); + } + } + } + } + + /// Keeps the policy of a ``. + fn meta_policy(&mut self, attributes: &[Attribute<'_>]) { + let document = self.document; + let value = |expected: &str| { + attributes + .iter() + .find(|attribute| attribute.name.eq_ignore_ascii_case(expected)) + .and_then(|attribute| attribute.value) + .and_then(|(start, end)| document.get(start..end)) + }; + let is_policy = value("http-equiv") + .is_some_and(|name| name.trim().eq_ignore_ascii_case("content-security-policy")); + if !is_policy { + return; + } + match value("content") { + Some(policy) if !policy.contains('&') => self.meta_policies.push(policy), + _ => self.unreadable_meta_policy = true, + } + } + + /// The media URLs of the CSS at `range`. + fn css(&mut self, range: (usize, usize), site: Site) { + for uri in css_urls(self.document, range) { + self.consider(uri, site, false); + } + } + + /// Classify the original outer SVG before returning any descendant edit. + /// All media collected since its start belongs to this range. A protected + /// or incomplete SVG is kept byte-for-byte, including root attributes, + /// CSS and nested SVGs; it is not submitted for a new whole-asset signature. + /// Returns whether the range is editable; only an explicit outer closing + /// tag makes it eligible for whole-SVG signing. + fn finish_svg( + &mut self, + start: usize, + end: usize, + complete: bool, + (found, inline, refused): (usize, usize, usize), + ) -> bool { + let refusal = self + .document + .get(start..end) + .map_or( + Some(Reason::InlineSvgNotEquivalent), + crate::svg_equivalence::descendant_edit_refusal, + ) + .or_else(|| (!complete).then_some(Reason::InlineSvgNotEquivalent)); + if let Some(reason) = refusal { + self.scan.found.truncate(found); + self.inline.truncate(inline); + self.scan.refused.truncate(refused); + self.scan.refused.push(reason); + self.scan.protected.push((start, end)); + false + } else { + true + } + } + + /// The scan, with each inline style URI kept only where `policies` (the + /// response's policy header values, `None` when unknown) and the page's + /// `` policies leave its style editable. + fn finish(self, policies: Option<&[String]>) -> Scan { + let Self { + document, + mut scan, + inline, + meta_policies, + unreadable_meta_policy, + } = self; + let all: Vec<&str> = policies + .unwrap_or_default() + .iter() + .map(String::as_str) + .chain(meta_policies) + .flat_map(|value| value.split(',')) + .collect(); + for (found, site) in inline { + let refusal = if policies.is_none() || unreadable_meta_policy { + Some(Reason::CspUnknown) + } else { + let locked = match site { + Site::StyleElement { nonce, .. } => { + let nonce = nonce.and_then(|(start, end)| document.get(start..end)); + locks(&all, &["style-src-elem", "style-src", "default-src"], nonce) + } + _ => locks(&all, &["style-src-attr", "style-src", "default-src"], None), + }; + locked.then_some(Reason::CspStyleHash) + }; + match refusal { + Some(reason) => scan.refused.push(reason), + None => scan.found.push(found), + } + } + scan.found.sort_by_key(|found| found.start); + scan + } +} + +/// Whether any of `policies` admits inline styles, under the first directive +/// of `order` it names, by hash, so an edit would block the style. The +/// element's `nonce` value admits it whatever its text where the directive +/// lists that exact nonce; another nonce does not. +fn locks(policies: &[&str], order: &[&str], nonce: Option<&str>) -> bool { + policies.iter().any(|policy| { + let directives: Vec<(&str, &str)> = policy + .split(';') + .map(|directive| { + let directive = directive.trim(); + directive + .split_once(|ch: char| ch.is_ascii_whitespace()) + .unwrap_or((directive, "")) + }) + .collect(); + let Some(sources) = order.iter().find_map(|wanted| { + directives + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case(wanted)) + .map(|(_, sources)| *sources) + }) else { + return false; + }; + let hashed = sources.split_ascii_whitespace().any(|source| { + ["'sha256-", "'sha384-", "'sha512-"].iter().any(|prefix| { + source + .get(..prefix.len()) + .is_some_and(|start| start.eq_ignore_ascii_case(prefix)) + }) + }); + // The `'nonce-` keyword is case-insensitive, its value is not. + let nonced = nonce.is_some_and(|value| { + !value.is_empty() + && sources.split_ascii_whitespace().any(|source| { + source + .get(..7) + .is_some_and(|start| start.eq_ignore_ascii_case("'nonce-")) + && source.get(7..).and_then(|rest| rest.strip_suffix('\'')) == Some(value) + }) + }); + hashed && !nonced + }) +} + +/// Whether the attribute `name` holds one URL a browser loads or links: +/// the standard URL attributes and the lazy-loading `data-*` ones that +/// scripts copy into them. A `data:` value in any other attribute, such as +/// `title`, `value` or `content`, is text and is left alone. +fn is_url_attribute(name: &str) -> bool { + [ + "src", + "href", + "xlink:href", + "poster", + "data", + "background", + "data-src", + "data-lazy-src", + "data-original", + "data-bg", + "data-background", + "data-poster", + "data-href", + ] + .iter() + .any(|candidate| name.eq_ignore_ascii_case(candidate)) +} + +/// The `Content-Security-Policy` and `-Report-Only` values of `headers`, as +/// [`scan_html`] reads them, or `None` when a value is not text and so +/// cannot be read. +pub(crate) fn content_security_policies(headers: &http::HeaderMap) -> Option> { + let mut policies = Vec::new(); + for name in [ + "content-security-policy", + "content-security-policy-report-only", + ] { + for value in headers.get_all(name) { + policies.push(value.to_str().ok()?.to_owned()); + } + } + Some(policies) +} + +/// Finds media in an HTML document. Inline style media is kept only when +/// `policies` (the response's policy header values, `None` when the module +/// cannot see them) and the page's `` policies admit no inline style +/// by hash, as an edit would block it. Original provenance-bearing or +/// structurally uncertain SVG ranges admit no edits, including descendants. +pub(crate) fn scan_html(document: &str, policies: Option<&[String]>) -> Scan { + let bytes = document.as_bytes(); + let mut collector = Collector::new(document); + let mut svg_depth = 0_usize; + let mut svg_start = 0_usize; + let mut svg_checkpoint = (0, 0, 0); + let mut cursor = 0_usize; + while cursor < bytes.len() { + if starts_with_at(bytes, cursor, b"\ + " + ); + let scan = scan_html(&document, None); + assert!(scan.is_empty(), "should find no media: {:?}", scan.found); + } + + #[test] + fn integrity_and_character_references_are_refused_not_dropped() { + let document = format!( + "\ + " + ); + let scan = scan_html(&document, None); + assert!(scan.found.is_empty(), "should rewrite neither"); + assert_eq!( + scan.refused, + vec![Reason::IntegrityAttribute, Reason::CharacterReference], + "should report each as unsigned" + ); + } + + #[test] + fn inline_style_edits_wait_for_known_policies_without_style_hashes() { + let document = format!( + "
\ + \ + " + ); + let unknown = scan_html(&document, None); + assert_eq!( + texts(&document, &unknown), + vec![PNG], + "should edit only the attribute" + ); + assert_eq!( + unknown.refused, + vec![Reason::CspUnknown, Reason::CspUnknown], + "should report both styles under an unseen policy" + ); + + let open = [String::from("img-src 'self' data:")]; + let scan = scan_html(&document, Some(&open)); + assert_eq!( + texts(&document, &scan), + vec![PNG; 3], + "should edit all, in order" + ); + assert!( + scan.found + .windows(2) + .all(|pair| pair[0].start < pair[1].start), + "should keep document order" + ); + + let hashed = [String::from("style-src 'sha256-abc' 'nonce-n'")]; + let scan = scan_html(&document, Some(&hashed)); + assert_eq!( + texts(&document, &scan).len(), + 2, + "should edit the image and the nonced element" + ); + assert_eq!(scan.refused, vec![Reason::CspStyleHash]); + + let meta = format!( + "{document}" + ); + let scan = scan_html(&meta, Some(&open)); + assert_eq!(texts(&meta, &scan), vec![PNG], "should honor a meta policy"); + } + + #[test] + fn only_the_style_s_own_nonce_admits_an_edit_under_a_hash_policy() { + let style = |nonce: &str| { + format!("") + }; + let policy = [String::from("style-src 'sha256-abc' 'NONCE-n0Nce'")]; + for (nonce, edited) in [ + ("n0Nce", true), + ("other", false), + ("n0nce", false), + ("", false), + ] { + let document = style(nonce); + let scan = scan_html(&document, Some(&policy)); + assert_eq!( + scan.found.len(), + usize::from(edited), + "nonce {nonce:?}: should edit only on the exact listed value" + ); + if !edited { + assert_eq!(scan.refused, vec![Reason::CspStyleHash], "nonce {nonce:?}"); + } + } + } + + #[test] + fn style_text_inside_inline_svg_is_markup_whose_references_are_decoded() { + let uri = "data:image/svg+xml,%3Csvg%3E&"; + let open = [String::from("img-src data:")]; + let in_svg = format!( + "" + ); + let scan = scan_html(&in_svg, Some(&open)); + assert!( + scan.found.is_empty(), + "should not rewrite a decoded reference" + ); + assert_eq!(scan.refused, vec![Reason::CharacterReference]); + + let in_html = format!(""); + let scan = scan_html(&in_html, Some(&open)); + assert_eq!( + texts(&in_html, &scan), + vec![uri], + "an HTML style's text is raw, so its `&` is literal" + ); + + let script = format!( + "" + ); + let scan = scan_html(&script, Some(&open)); + assert_eq!( + texts(&script, &scan), + vec![PNG], + "should keep scanning after an SVG script" + ); + } + + #[test] + fn only_url_attributes_are_rewritten() { + let document = format!( + "\ + \"{PNG}\"\ +
" + ); + let scan = scan_html(&document, None); + let found: Vec = scan.found.iter().map(|found| found.start).collect(); + let lazy = document.find("data-lazy-src=\"").expect("lazy") + "data-lazy-src=\"".len(); + let background = document.find("data-bg=\"").expect("bg") + "data-bg=\"".len(); + let object = document.find("data=\"data:").expect("object") + "data=\"".len(); + assert_eq!( + found, + vec![lazy, background, object], + "should rewrite lazy, background and object URLs, never text attributes" + ); + } + + #[test] + fn policies_are_read_from_both_policy_headers() { + let mut headers = http::HeaderMap::new(); + headers.append( + "content-security-policy", + http::HeaderValue::from_static("img-src data:"), + ); + headers.append( + "content-security-policy-report-only", + http::HeaderValue::from_static("style-src 'sha256-abc'"), + ); + assert_eq!( + content_security_policies(&headers), + Some(vec![ + "img-src data:".to_owned(), + "style-src 'sha256-abc'".to_owned() + ]) + ); + } + + #[test] + fn stylesheets_find_url_and_image_set_media_outside_comments() { + let document = format!( + ".a{{background:url({PNG})}}/* url({PNG}) */.b{{background:url( \"{PNG}\" )}}\ + .c{{background-image:image-set(\"{PNG}\" 1x, url({PNG}) 2x)}}\ + .d{{background:url(data:image/png;base64,iVBO\\52w0KGgo=)}}.e{{content:\"{PNG}\"}}" + ); + let scan = scan_stylesheet(&document); + assert_eq!( + texts(&document, &scan), + vec![PNG; 4], + "should find each media URL" + ); + assert_eq!( + scan.refused, + vec![Reason::CssEscape], + "should refuse the escaped URL" + ); + } + + #[test] + fn outermost_inline_svgs_are_found_with_exact_ranges() { + let svg = + ""; + let document = format!("

a

{svg}

b

"); + let scan = scan_html(&document, None); + assert_eq!(scan.svgs.len(), 1, "should find one outermost element"); + let (start, end) = scan.svgs[0]; + assert_eq!(&document[start..end], svg, "should cover the exact bytes"); + } + + #[test] + fn self_closing_svg_attributes_are_scanned_without_whole_asset_signing() { + let document = format!(""); + let scan = scan_html(&document, Some(&[])); + assert_eq!(texts(&document, &scan), [PNG]); + assert!(scan.svgs.is_empty()); + assert!(scan.refused.is_empty()); + } + + #[test] + fn original_svg_provenance_protects_attributes_css_and_nested_svg_ranges() { + for provenance in [ + CARRIER, + "", + "https://example.com/m", + "", + "", + "QUJD", + ] { + let svg = format!( + "{ROOT}{provenance}\ + \ + " + ); + let document = format!("{svg}"); + let scan = scan_html(&document, Some(&[])); + assert_eq!(texts(&document, &scan), [PNG, PNG], "{provenance}"); + assert!(scan.svgs.is_empty(), "must not resign {provenance}"); + assert_eq!( + scan.refused, + [Reason::SvgExistingProvenance], + "{provenance}" + ); + } + } + + #[test] + fn unsigned_svg_descendants_remain_editable_without_a_false_caption_match() { + let document = format!( + "{ROOT}Signed with C2PA\ + \ + " + ); + let scan = scan_html(&document, Some(&[])); + assert_eq!(texts(&document, &scan), [PNG; 3]); + assert_eq!(scan.svgs, [(0, document.len())]); + assert!(scan.refused.is_empty()); + } + + #[test] + fn incomplete_or_uninspectable_svg_ranges_fail_closed_before_child_edits() { + for svg in [ + format!("{ROOT}{CARRIER}"), + format!("{ROOT}"), + format!("{ROOT}

breakout

"), + format!( + "{ROOT}{}", + "".repeat(10_000) + ), + format!(""), + ] { + let document = format!("{svg}"); + let scan = scan_html(&document, Some(&[])); + assert_eq!(texts(&document, &scan), [PNG], "{svg}"); + assert!(scan.svgs.is_empty(), "must not resign {svg}"); + assert_eq!(scan.refused.len(), 1); + } + } + + const ROOT: &str = ""; + const BODY: &str = ""; + const CARRIER: &str = "QUJD+/=="; + + #[test] + fn the_writer_s_carrier_after_the_root_start_tag_keeps_the_svg() { + let original = format!("{ROOT}{BODY}"); + let signed = format!("{ROOT}{CARRIER}{BODY}"); + assert_eq!( + verify_svg_carrier(original.as_bytes(), signed.as_bytes()), + Ok(()), + "should accept the writer's carrier" + ); + let declared = format!("\n{ROOT}{BODY}"); + let signed = format!("\n{ROOT}{CARRIER}{BODY}"); + assert_eq!( + verify_svg_carrier(declared.as_bytes(), signed.as_bytes()), + Ok(()), + "should find the root after a declaration and a comment" + ); + } + + #[test] + fn a_carrier_the_source_held_is_replaced_and_nothing_else() { + let old_element = "T0xE"; + for old in [old_element, ""] { + let original = format!("{ROOT}{old}{BODY}"); + let signed = format!("{ROOT}{CARRIER}{BODY}"); + assert_eq!( + verify_svg_carrier(original.as_bytes(), signed.as_bytes()), + Ok(()), + "should accept replacing {old}" + ); + } + } + + #[test] + fn any_other_change_to_the_svg_is_refused() { + let original = format!("{ROOT}{BODY}"); + let altered = [ + format!("{ROOT}{CARRIER}{BODY}").replace("currentColor", "#000"), + format!("{ROOT}{}{CARRIER}", BODY.replace("", "")) + "", + format!("{ROOT}{BODY}"), + format!("{ROOT}{}{BODY}", CARRIER.replace("QUJD+/==", "QU JD")), + format!("{ROOT} {CARRIER}{BODY}"), + format!("{ROOT}{BODY}"), + original.clone(), + ]; + for signed in altered { + assert_eq!( + verify_svg_carrier(original.as_bytes(), signed.as_bytes()), + Err(Reason::InlineSvgAltered), + "should refuse {signed}" + ); + } + } + + #[test] + fn splice_replaces_exact_ranges_within_budget() { + let document = ""; + let edits = vec![(10, 11, "xx".to_owned()), (23, 24, "y".to_owned())]; + assert_eq!( + splice(document, &edits, 100).as_deref(), + Some("") + ); + assert_eq!( + splice(document, &edits, document.len()), + None, + "should honor the budget" + ); + } +} diff --git a/crates/provenance/encypher/src/fixtures/parity.json b/crates/provenance/encypher/src/fixtures/parity.json new file mode 100644 index 0000000000..8487676317 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/parity.json @@ -0,0 +1,848 @@ +{ + "generated_from": [ + "integrations/edge-core/boundary.js", + "integrations/edge-core/fragments.js", + "integrations/edge-core/embed.js" + ], + "fixed_plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "cases": [ + { + "id": "override-multibyte-prefix", + "html": "Café & News🙂前

Alpha & beta.

", + "override_selector": "[data=story]", + "boundary": { + "selector": "override:[data=story]", + "start": 78, + "end": 126 + }, + "text": "Alpha & beta.", + "text_rust": "Alpha & beta.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "Café & News🙂前

A︀︁lpha & beta.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "Café & News🙂前

Alpha & beta.

", + "expected_meta_output_rust": "Café & News🙂前

Alpha & beta.

", + "meta_divergence": null + }, + { + "id": "article-most-paragraphs", + "html": "Two articles

Short.

First paragraph.

Second paragraph.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 84, + "end": 159 + }, + "text": "First paragraph. Second paragraph .", + "text_rust": "First paragraph. Second paragraph .", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "Two articles

Short.

F︀︁irst paragraph.

Second paragraph.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "Two articles

Short.

First paragraph.

Second paragraph.

", + "expected_meta_output_rust": "Two articles

Short.

First paragraph.

Second paragraph.

", + "meta_divergence": null + }, + { + "id": "cms-class-entities", + "html": "

Fish & chips — © 🐟.

", + "override_selector": null, + "boundary": { + "selector": "class:entry-content", + "start": 25, + "end": 123 + }, + "text": "Fish & chips — © 🐟.", + "text_rust": "Fish & chips — © 🐟.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

F︀︁ish & chips — © 🐟.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Fish & chips — © 🐟.

", + "expected_meta_output_rust": "

Fish & chips — © 🐟.

", + "meta_divergence": null + }, + { + "id": "itemprop-nbsp", + "html": "

One two three.

", + "override_selector": null, + "boundary": { + "selector": "itemprop:articleBody", + "start": 25, + "end": 94 + }, + "text": "One two three.", + "text_rust": "One two three.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

O︀︁ne two three.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

One two three.

", + "expected_meta_output_rust": "

One two three.

", + "meta_divergence": null + }, + { + "id": "main-inline-tags", + "html": "

Inline

Hello fictional example.com.

", + "override_selector": null, + "boundary": { + "selector": "main", + "start": 25, + "end": 105 + }, + "text": "Inline Hello fictional example.com.", + "text_rust": "Inline Hello fictional example.com.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

I︀︁nline

Hello fictional example.com.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Inline

Hello fictional example.com.

", + "expected_meta_output_rust": "

Inline

Hello fictional example.com.

", + "meta_divergence": null + }, + { + "id": "role-main", + "html": "

Role based article boundary.

", + "override_selector": null, + "boundary": { + "selector": "role:main", + "start": 25, + "end": 83 + }, + "text": "Role based article boundary.", + "text_rust": "Role based article boundary.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

R︀︁ole based article boundary.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Role based article boundary.

", + "expected_meta_output_rust": "

Role based article boundary.

", + "meta_divergence": null + }, + { + "id": "paragraph-cluster", + "html": "

One.

Two.

Three.

Four.

Five.

", + "override_selector": null, + "boundary": { + "selector": "p-cluster", + "start": 25, + "end": 109 + }, + "text": "One. Two. Three. Four. Five.", + "text_rust": "One. Two. Three. Four. Five.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

O︀︁ne.

Two.

Three.

Four.

Five.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

One.

Two.

Three.

Four.

Five.

", + "expected_meta_output_rust": "

One.

Two.

Three.

Four.

Five.

", + "meta_divergence": null + }, + { + "id": "body-fallback", + "html": "

Fallback

Body article text.", + "override_selector": null, + "boundary": { + "selector": "body", + "start": 19, + "end": 80 + }, + "text": "Fallback Body article text.", + "text_rust": "Fallback Body article text.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

F︀︁allback

Body article text.", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Fallback

Body article text.", + "expected_meta_output_rust": "

Fallback

Body article text.", + "meta_divergence": null + }, + { + "id": "nested-same-name-first-close", + "html": "

Outer start.

Inner text.

Outer tail omitted by parity.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 90 + }, + "text": "Outer start. Inner text.", + "text_rust": "Outer start. Inner text.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

O︀︁uter start.

Inner text.

Outer tail omitted by parity.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Outer start.

Inner text.

Outer tail omitted by parity.

", + "expected_meta_output_rust": "

Outer start.

Inner text.

Outer tail omitted by parity.

", + "meta_divergence": null + }, + { + "id": "head-close-inside-script", + "html": "Script case

Script boundary fallback.

", + "override_selector": null, + "boundary": { + "selector": "body", + "start": 86, + "end": 131 + }, + "text": "Script boundary fallback.", + "text_rust": "Script boundary fallback.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "Script case

S︀︁cript boundary fallback.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "Script case

Script boundary fallback.

", + "expected_meta_output_rust": "Script case

Script boundary fallback.

", + "meta_divergence": "JS inserts at a closing-head sequence inside script text; Rust inserts before the structural closing head tag." + }, + { + "id": "head-close-inside-comment", + "html": "Comment case

Comment boundary fallback.

", + "override_selector": null, + "boundary": { + "selector": "body", + "start": 80, + "end": 126 + }, + "text": "Comment boundary fallback.", + "text_rust": "Comment boundary fallback.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "Comment case

C︀︁omment boundary fallback.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "Comment case

Comment boundary fallback.

", + "expected_meta_output_rust": "Comment case

Comment boundary fallback.

", + "meta_divergence": "JS inserts at a closing-head sequence inside a comment; Rust inserts before the structural closing head tag." + }, + { + "id": "head-close-inside-title", + "html": "Headline </head> text

Title boundary fallback.

", + "override_selector": null, + "boundary": { + "selector": "body", + "start": 55, + "end": 99 + }, + "text": "Title boundary fallback.", + "text_rust": "Title boundary fallback.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "Headline </head> text

T︀︁itle boundary fallback.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "Headline <meta name=\"c2pa-manifest-url\" content=\"https://manifest.example.com/c2pa?id=fictional&kind="article"\"></head> text

Title boundary fallback.

", + "expected_meta_output_rust": "Headline </head> text

Title boundary fallback.

", + "meta_divergence": "JS inserts at a closing-head sequence inside title text; Rust never searches raw-text or RCDATA content for the closing head tag." + }, + { + "id": "javascript-whitespace-set", + "html": "

Alpha 
beta\t gamma.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 77 + }, + "text": "Alpha beta gamma.", + "text_rust": "Alpha beta gamma.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

A︀︁lpha 
beta\t gamma.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Alpha 
beta\t gamma.

", + "expected_meta_output_rust": "

Alpha 
beta\t gamma.

", + "meta_divergence": null + }, + { + "id": "skip-raw-elements-and-numeric-entities", + "html": "

Visible A.

Hidden code.
Hidden pre.

Tail B.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 162 + }, + "text": "Visible A. Tail B.", + "text_rust": "Visible A. Tail B.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

V︀︁isible A.

Hidden code.
Hidden pre.

Tail B.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Visible A.

Hidden code.
Hidden pre.

Tail B.

", + "expected_meta_output_rust": "

Visible A.

Hidden code.
Hidden pre.

Tail B.

", + "meta_divergence": null + }, + { + "id": "only-the-seven-skip-elements-are-hidden", + "html": "

Lead text.

Inline title.

After.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 166 + }, + "text": "Lead text. Draft note. Frame fallback. Inline title. After.", + "text_rust": "Lead text. Draft note. Frame fallback. Inline title. After.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

L︀︁ead text.

Inline title.

After.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Lead text.

Inline title.

After.

", + "expected_meta_output_rust": "

Lead text.

Inline title.

After.

", + "meta_divergence": null + }, + { + "id": "self-closing-svg-hides-nothing", + "html": "

Lead text.

After icon.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 103 + }, + "text": "Lead text. After icon.", + "text_rust": "Lead text. After icon.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

L︀︁ead text.

After icon.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Lead text.

After icon.

", + "expected_meta_output_rust": "

Lead text.

After icon.

", + "meta_divergence": null + }, + { + "id": "quoted-gt-in-attribute", + "html": "

Lead text.

\"a b\">Tail text.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 95 + }, + "text": "Lead text. b\">Tail text.", + "text_rust": "Lead text. Tail text.", + "text_divergence": "JS ends a tag at the first \">\", so the rest of the alt value becomes text; Rust ends it at the first \">\" outside a quoted attribute value.", + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

L︀︁ead text.

\"a b\">Tail text.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Lead text.

\"a b\">Tail text.

", + "expected_meta_output_rust": "

Lead text.

\"a b\">Tail text.

", + "meta_divergence": null + }, + { + "id": "nested-same-name-ignored-element", + "html": "

Lead text.

outer inner tail

After.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 116 + }, + "text": "Lead text. After.", + "text_rust": "Lead text. tail After.", + "text_divergence": "JS counts nested start and end tags and drops the outer tail; Rust closes the ignored region at the first matching end tag.", + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

L︀︁ead text.

outer inner tail

After.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Lead text.

outer inner tail

After.

", + "expected_meta_output_rust": "

Lead text.

outer inner tail

After.

", + "meta_divergence": null + }, + { + "id": "self-closing-script-stays-raw-text", + "html": "

Lead text.

After.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 107 + }, + "text": "Lead text. var hidden = 1; After.", + "text_rust": "Lead text. After.", + "text_divergence": "JS treats \".", + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

L︀︁ead text.

After.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Lead text.

After.

", + "expected_meta_output_rust": "

Lead text.

After.

", + "meta_divergence": null + }, + { + "id": "end-tag-with-attributes-closes-ignored-element", + "html": "

Lead text.

After.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 132 + }, + "text": "Lead text. After.", + "text_rust": "Lead text. After.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

L︀︁ead text.

After.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Lead text.

After.

", + "expected_meta_output_rust": "

Lead text.

After.

", + "meta_divergence": null + }, + { + "id": "abrupt-and-bang-comment-ends", + "html": "

Lead text.

Shown one.Shown two.Shown three.

After.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 131 + }, + "text": "Lead text. Shown two.", + "text_rust": "Lead text. Shown one. Shown two. Shown three. After.", + "text_divergence": "JS ends every comment at the next \"-->\"; Rust, like browsers, treats \"\" and \"\" as whole comments and also ends a comment at \"--!>\".", + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

L︀︁ead text.

Shown one.Shown two.Shown three.

After.

", + "invalid_plan": null, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Lead text.

Shown one.Shown two.Shown three.

After.

", + "expected_meta_output_rust": "

Lead text.

Shown one.Shown two.Shown three.

After.

", + "meta_divergence": null + }, + { + "id": "out-of-range-index", + "html": "

Small boundary.

", + "override_selector": null, + "boundary": { + "selector": "article", + "start": 25, + "end": 66 + }, + "text": "Small boundary.", + "text_rust": "Small boundary.", + "text_divergence": null, + "plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": -1, + "marker": "" + }, + { + "insert_after_index": 0, + "marker": "︀" + }, + { + "insert_after_index": 0, + "marker": "︁" + } + ] + }, + "expected_output": "

S︀︁mall boundary.

", + "invalid_plan": { + "index_unit": "codepoint", + "operations": [ + { + "insert_after_index": 15, + "marker": "︀" + } + ] + }, + "expected_invalid_output": null, + "manifest_url": "https://manifest.example.com/c2pa?id=fictional&kind=\"article\"", + "expected_meta_output_js": "

Small boundary.

", + "expected_meta_output_rust": "

Small boundary.

", + "meta_divergence": null + } + ] +} diff --git a/crates/provenance/encypher/src/fixtures/prepared/descriptor.json b/crates/provenance/encypher/src/fixtures/prepared/descriptor.json new file mode 100644 index 0000000000..25ef1b302e --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/descriptor.json @@ -0,0 +1,7 @@ +{ + "abi": 1, + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "release": 18, + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f", + "signature": "33Dab6p1n5yJpIbXypes+yr6xXalcFonsh5h9YaFzMo18zFMHlNDXzriZNOe5LHZQ03P7Ki0T4ZXT+QNS6okCw==" +} diff --git a/crates/provenance/encypher/src/fixtures/prepared/external-png/envelope.json b/crates/provenance/encypher/src/fixtures/prepared/external-png/envelope.json new file mode 100644 index 0000000000..f602ac1eb4 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/external-png/envelope.json @@ -0,0 +1,78 @@ +{ + "deployment_id": "async-prepared-api-fixture", + "identity": "bcb02ca8a4fd668d75823760fe036f8cbe7b4a2e2425cf119ff2f787fa9992df", + "item": { + "kind": "media", + "planned": { + "digest": "339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd", + "geometry": { + "binding_recipe": { + "algorithm": "sha256", + "exclusions": [], + "type": "data_hash" + }, + "carrier_output_span": [ + 0, + 0 + ], + "carrier_recipe": null, + "carrier_span": [ + 0, + 0 + ], + "disposition": "external", + "edits": [], + "expected_final_size": 205, + "planner_options": { + "tiff_placement": "main_ifd_when_single" + } + }, + "idempotency_key": "async-prepared-fixture:external-png:d243b83eb5d94188ace4ab03b219c8a6", + "plan_id": "400be3dc-9766-4eb3-85b9-8a7b9a668ccb", + "release": { + "abi": 1, + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "release": 18, + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f", + "signature": "33Dab6p1n5yJpIbXypes+yr6xXalcFonsh5h9YaFzMo18zFMHlNDXzriZNOe5LHZQ03P7Ki0T4ZXT+QNS6okCw==" + }, + "request": { + "carrier_requirement": "external_allowed", + "contract_version": 2, + "expected_policy_digest": "171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff", + "page_url": "https://async-prepared-fixture.publisher.example.com/article", + "probe": { + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f" + }, + "protocol": "encypher-prepared-signing/2", + "source": { + "byte_length": 205, + "mime": "image/png", + "origin_host": "async-prepared-fixture.publisher.example.com", + "reference": "https://async-prepared-fixture.publisher.example.com/article", + "reference_kind": "origin", + "sha256": "339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd" + }, + "structure": [ + { + "b64": "iVBORw0KGgoAAAANSUhEUg==", + "offset": 0 + }, + { + "b64": "27ZsSwAAAJRJREFU", + "offset": 29 + }, + { + "b64": "6qgrjgAAAABJRU5ErkJggg==", + "offset": 189 + } + ] + } + }, + "source_b64": "iVBORw0KGgoAAAANSUhEUgAAADAAAAAgCAIAAADbtmxLAAAAlElEQVR4nO3XywqAIBCFYS8dWsz7v29bJS+Bo3MSYRbRYvz4KSLvnEMQnrni7WMgmp1BwgaiK7Qn6LxlPwOBDcRVCFQgEBbSBQ19N5Bcb1tIJ489KNeIPeg9liCUbp5CzTxmoJrGshARqJFHCzT8v5EYVhfK8pSOXgpCBWEB6p8i80H55vazPLPQ2EIlUG/Jxzwx+AdPEifC6qgrjgAAAABJRU5ErkJggg==" + }, + "protocol": "encypher-preparation/1", + "retirement_epoch": 0, + "variant": "" +} diff --git a/crates/provenance/encypher/src/fixtures/prepared/external-png/finalize.json b/crates/provenance/encypher/src/fixtures/prepared/external-png/finalize.json new file mode 100644 index 0000000000..5c19ed02ab --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/external-png/finalize.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"400be3dc-9766-4eb3-85b9-8a7b9a668ccb","evidence_id":"c1c964b3-525d-45e8-8ace-a95159f237f7","instance_id":"urn:uuid:35ea4c05-95dc-47a2-b1d0-fb983015d563","document_id":"doc_71bfc6826448e889e17e032cd50b2a5c","asset_id":"doc_71bfc6826448e889e17e032cd50b2a5c","mime":"image/png","manifest_disposition":"external","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd","binding":{"type":"data_hash","digest":"339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd"},"manifest_store_b64":"AACQn2p1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJB5anVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo5NDYwY2JiNi1mZDEyLTRmOWMtODFhMy03MjViOWJkZjI1MDUAAAAETmp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFgghUAwhApsoPtfB10hw0vEIXykZSJA9RCxVQqoNtH/gfpyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhOQAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY183MWJmYzY4MjY0NDhlODg5ZTE3ZTAzMmNkNTBiMmE1Y2hpbWFnZV9pZHgkZG9jXzcxYmZjNjgyNjQ0OGU4ODllMTdlMDMyY2Q1MGIyYTVjaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMxWgAAAQ1qdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAADZY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxlZ2FydGljbGVpZGM6Zm9ybWF0aWltYWdlL3BuZ2RkYXRhpWN1cmx4PGh0dHBzOi8vYXN5bmMtcHJlcGFyZWQtZml4dHVyZS5wdWJsaXNoZXIuZXhhbXBsZS5jb20vYXJ0aWNsZWNhbGdmc2hhMjU2ZGhhc2hYIDOb+6o6IYjskDLXs6BOOIptFmj6uYlvNitRkAd3hqe9aWRjOmZvcm1hdGlpbWFnZS9wbmdkc2l6ZRjNAAAAcGp1bWIAAAAoanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5oYXNoLmRhdGEAAAAAQGNib3KjY2FsZ2ZzaGEyNTZkaGFzaFggM5v7qjohiOyQMtezoE44im0WaPq5iW82K1GQB3eGp71jcGFkQAAAA05qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADH2Nib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOjM1ZWE0YzA1LTk1ZGMtNDdhMi1iMWQwLWZiOTgzMDE1ZDU2M3RjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYTo5NDYwY2JiNi1mZDEyLTRmOWMtODFhMy03MjViOWJkZjI1MDUvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgge4AXmcNsM+BPV3W5NDqTMFpxyE3E7Szko3sfK6anI1ZjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIJvdtHwPIJmc9nJG7+j9LVRbtseDJDyV18spkUR7WwQKY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFggeHHxhpG+8jZF3gujAhtKeVwcGKHgunicOMNRh2vbFW1jYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFgghUAwhApsoPtfB10hw0vEIXykZSJA9RCxVQqoNtH/gfpjYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCBxN6xzfVNNnart1gmsRI1cPl7BESqzHAzYPwdevkIsAmNhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGVnYXJ0aWNsZQAAiI5qdW1iAAAAKGp1bWRjMmNzABEAEIAAAKoAOJtxA2MycGEuc2lnbmF0dXJlAAAAiF5jYm9y0oRZB+WiATgiGCGCWQPtMIID6TCCA26gAwIBAgIUGX8Yv3cXfmzZcGD3B8bKBCbPVWowCgYIKoZIzj0EAwMwgZExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAkBgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMzAxNDEyOVowPzELMAkGA1UEBhMCVVMxHTAbBgNVBAoMFEVuY3lwaGVyIENvcnBvcmF0aW9uMREwDwYDVQQDDAhFbmN5cGhlcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABDvZ7+eBfyDr2C485geb984SGNesQNv+ymO3i3/flJuNNrIcSQmWDm44Vo6mUkaCGGKJzk42Zdxb+Z0cmJjpK6VNqd9JECoe5V9dGv8KxfQIyx+ixJfAV4I/KHEeW9pCBKOCAdYwggHSMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUCriv8eHAu/yzi9Pp2awmraA9GgowHwYDVR0jBBgwFoAUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvaXNzdWluZy1jYS5jcmwwdQYIKwYBBQUHAQEEaTBnMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwPAYIKwYBBQUHMAKGMGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9pc3N1aW5nLWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwGQYJKwYBBAGD6F4DBAwGCisGAQQBg+heAwowMwYJKwYBBAGD6F4EBCYMJDAxOWQyMjQxLWViOTctNzcyOC05ZWMwLWNkYWFmYmEzMDBjMjAKBggqhkjOPQQDAwNpADBmAjEA9e9693z4UoLL2Fe9BFkcbglh+CfLRNjlulgE/VPyLVrxgb/FattEgkhMrWIyfQmiAjEA+JtMHY+rwKS/P9o2MbVfSH5tvXcTrXkpPqpaHTVW7g/LK2Pf+dMuRbVlii70549hWQPrMIID5zCCA26gAwIBAgIUT31Fs3EI6FA6i/fOEM+G8cUbSzAwCgYIKoZIzj0EAwMwgY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xIzAhBgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTMxMTEwMzAxNDEyOVowgZExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAkBgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE56GYjZM+Em8LMeptCt79IRV7CTKWC71zG9B2HlQLwm6UNDgSjzBokSvS6l7csnRvixB8ht8nW51dKuAUoPi8RzweUMCPtT28K7TNdeOTDX/kew2Y8PR9Grc1fLc6zAK6o4IBhjCCAYIwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lBBkwFwYKKwYBBAGD6F4CAQYJKoZIhvcvAQEFMB0GA1UdDgQWBBRlDB/oY/6Ws3C27VKnp+CheGDcSTAfBgNVHSMEGDAWgBTobldnKwXMjAtYeHF8tZF037lqHjA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC9yb290LWNhLmNybDByBggrBgEFBQcBAQRmMGQwJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDA5BggrBgEFBQcwAoYtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L3Jvb3QtY2EuY3J0ME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczAKBggqhkjOPQQDAwNnADBkAjBXIrL49fp6vJohGJ94SLlWBs6CFSMSfkACfYbbAdWroF/+WE7e1h/JjdfnGFxbdvwCMC5bizhQibaWWOn9N5JzEoFEKK6PhRMTxwZAk23ZLWvMWae04/5Sjs4wYqfUN26ckKJnc2lnVHN0MqFpdHN0VG9rZW5zgaFjdmFsWQahMIIGnQYJKoZIhvcNAQcCoIIGjjCCBooCAQMxDzANBglghkgBZQMEAgIFADCBiwYLKoZIhvcNAQkQAQSgfAR6MHgCAQEGCisGAQQBg+heBQEwMTANBglghkgBZQMEAgEFAAQgyy32ihJdAK6kjKiVsfnECyayW3qTRi2ysysVxmtqQ6ICFFhj+oFRGYbBW7Mz6x01R+Djl5rRGA8yMDI2MTEwMjAxNDEzMVowAwIBAQIITIqP2m7uhyugggPsMIID6DCCA26gAwIBAgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swCgYIKoZIzj0EAwMwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDIwMTQxMjlaMIGMMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSEwHwYDVQQDDBhFbmN5cGhlciBDMlBBIFRTQSBTaWduZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARr9gYQm+do/W6EIeKN2Uf5q9n8BoHmRqwDR/aTjv7mnsjg5PYBpoehnpzUp6iFBWDe8KAyOTjUx97QD60CJu2FULupHqIFTd5Lr8TD2903S/vL3PdOLM4mqhB/kyoxo4ajggGEMIIBgDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQUMtB7qVGEl2DENzmiThKHM9aMEK8wHwYDVR0jBBgwFoAUGVYfWNddcK6pMhCmHPMntLdClEswTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMHkGCCsGAQUFBwEBBG0wazAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMEAGCCsGAQUFBzAChjRodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvdHNhLWlzc3VpbmctY2EuY3J0MD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3RzYS1pc3N1aW5nLWNhLmNybDAKBggqhkjOPQQDAwNoADBlAjAxltHmUNm63uJrlWppEXbzH8U63vLO/KuiKZh7OyZLWJ73ocBZLrd0IHSxCz3Sln0CMQCYi4S4zwOX0Fg9xQh6/DLMy0O6jInZxIin+OGCbDxdhteAor5E8LFQj/VQwNqkGz0xggH0MIIB8AIBATCBrjCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2AhQoeKpdmqtzWZUbLWXhGy79mL0vizANBglghkgBZQMEAgIFAKCBtjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHgYJKoZIhvcNAQkFMREYDzIwMjYxMTAyMDE0MTMxWjA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCA8afx2houUS/YnXhA2Im3weOs9XV+BouMu8iSEfEywUjA/BgkqhkiG9w0BCQQxMgQwoKj6VeQx687HGSGNPQ3RLT7M3nu+yttSjdkgK3BWl4ZJ5zQsxFKqhaZ65kEWiV1jMAoGCCqGSM49BAMDBGYwZAIwIFtihmnx0IyZvsqBZcpVuEDosVYMk3Wswuen+XBzO7nDTFXCQEwKpiZSF0xil5z7AjAVIfpKNkuxGpcqvX5Ssh7/aCEwBxYv/Jk2mpFglewmGPqqpn5CNlhpDYKJkLywynYaQ1BBZFl5QwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9lhg4DpfNm6/b6faVGyYrF9Mpqg/2Y9gq89VU4siKvXGKGZbRGzNM3ypp4ZPEZ3NJ+NfULxYA/aBbWRRonWDpIFB8bPH/0TgZd5SbzWXHQ8HfJhsxYRaM/2/9F9dkaIolCRx","manifest_sha256":"ccbe864706aa5cfbebefe9429dfcad06edf9f0b4a6a72fb437e8fe3a4281f04b","manifest_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/manifests/external/ref/7de43f3b4c6246dcae7d3a1d6cd32823","xmp_reference":"declined","expected_final_size":205,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_71bfc6826448e889e17e032cd50b2a5c","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/external-png/output.png b/crates/provenance/encypher/src/fixtures/prepared/external-png/output.png new file mode 100644 index 0000000000..0b577c44c1 Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/external-png/output.png differ diff --git a/crates/provenance/encypher/src/fixtures/prepared/external-png/prepare.json b/crates/provenance/encypher/src/fixtures/prepared/external-png/prepare.json new file mode 100644 index 0000000000..d85480d430 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/external-png/prepare.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"400be3dc-9766-4eb3-85b9-8a7b9a668ccb","instance_id":"urn:uuid:35ea4c05-95dc-47a2-b1d0-fb983015d563","document_id":"doc_71bfc6826448e889e17e032cd50b2a5c","expires_at":"2026-11-02T02:41:31.514051Z","mime":"image/png","expected_manifest_size":37023,"effective_action":"c2pa.opened","geometry":{"disposition":"external","edits":[],"carrier_span":[0,0],"carrier_output_span":[0,0],"expected_final_size":205,"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[]},"carrier_recipe":null,"planner_options":{"tiff_placement":"main_ifd_when_single"}},"local_ops":[],"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/external-png/recover.json b/crates/provenance/encypher/src/fixtures/prepared/external-png/recover.json new file mode 100644 index 0000000000..aba4bbeee4 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/external-png/recover.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff","prepare":{"protocol":"encypher-prepared-signing/2","plan_id":"400be3dc-9766-4eb3-85b9-8a7b9a668ccb","instance_id":"urn:uuid:35ea4c05-95dc-47a2-b1d0-fb983015d563","document_id":"doc_71bfc6826448e889e17e032cd50b2a5c","expires_at":"2026-11-02T02:41:31.514051Z","mime":"image/png","expected_manifest_size":37023,"effective_action":"c2pa.opened","geometry":{"edits":[],"disposition":"external","carrier_span":[0,0],"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[]},"carrier_recipe":null,"planner_options":{"tiff_placement":"main_ifd_when_single"},"carrier_output_span":[0,0],"expected_final_size":205},"local_ops":[],"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"},"finalize":{"protocol":"encypher-prepared-signing/2","plan_id":"400be3dc-9766-4eb3-85b9-8a7b9a668ccb","evidence_id":"c1c964b3-525d-45e8-8ace-a95159f237f7","instance_id":"urn:uuid:35ea4c05-95dc-47a2-b1d0-fb983015d563","document_id":"doc_71bfc6826448e889e17e032cd50b2a5c","asset_id":"doc_71bfc6826448e889e17e032cd50b2a5c","mime":"image/png","manifest_disposition":"external","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd","binding":{"type":"data_hash","digest":"339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd"},"manifest_store_b64":"AACQn2p1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJB5anVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo5NDYwY2JiNi1mZDEyLTRmOWMtODFhMy03MjViOWJkZjI1MDUAAAAETmp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFgghUAwhApsoPtfB10hw0vEIXykZSJA9RCxVQqoNtH/gfpyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhOQAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY183MWJmYzY4MjY0NDhlODg5ZTE3ZTAzMmNkNTBiMmE1Y2hpbWFnZV9pZHgkZG9jXzcxYmZjNjgyNjQ0OGU4ODllMTdlMDMyY2Q1MGIyYTVjaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMxWgAAAQ1qdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAADZY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxlZ2FydGljbGVpZGM6Zm9ybWF0aWltYWdlL3BuZ2RkYXRhpWN1cmx4PGh0dHBzOi8vYXN5bmMtcHJlcGFyZWQtZml4dHVyZS5wdWJsaXNoZXIuZXhhbXBsZS5jb20vYXJ0aWNsZWNhbGdmc2hhMjU2ZGhhc2hYIDOb+6o6IYjskDLXs6BOOIptFmj6uYlvNitRkAd3hqe9aWRjOmZvcm1hdGlpbWFnZS9wbmdkc2l6ZRjNAAAAcGp1bWIAAAAoanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5oYXNoLmRhdGEAAAAAQGNib3KjY2FsZ2ZzaGEyNTZkaGFzaFggM5v7qjohiOyQMtezoE44im0WaPq5iW82K1GQB3eGp71jcGFkQAAAA05qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADH2Nib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOjM1ZWE0YzA1LTk1ZGMtNDdhMi1iMWQwLWZiOTgzMDE1ZDU2M3RjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYTo5NDYwY2JiNi1mZDEyLTRmOWMtODFhMy03MjViOWJkZjI1MDUvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgge4AXmcNsM+BPV3W5NDqTMFpxyE3E7Szko3sfK6anI1ZjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIJvdtHwPIJmc9nJG7+j9LVRbtseDJDyV18spkUR7WwQKY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFggeHHxhpG+8jZF3gujAhtKeVwcGKHgunicOMNRh2vbFW1jYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFgghUAwhApsoPtfB10hw0vEIXykZSJA9RCxVQqoNtH/gfpjYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCBxN6xzfVNNnart1gmsRI1cPl7BESqzHAzYPwdevkIsAmNhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGVnYXJ0aWNsZQAAiI5qdW1iAAAAKGp1bWRjMmNzABEAEIAAAKoAOJtxA2MycGEuc2lnbmF0dXJlAAAAiF5jYm9y0oRZB+WiATgiGCGCWQPtMIID6TCCA26gAwIBAgIUGX8Yv3cXfmzZcGD3B8bKBCbPVWowCgYIKoZIzj0EAwMwgZExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAkBgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMzAxNDEyOVowPzELMAkGA1UEBhMCVVMxHTAbBgNVBAoMFEVuY3lwaGVyIENvcnBvcmF0aW9uMREwDwYDVQQDDAhFbmN5cGhlcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABDvZ7+eBfyDr2C485geb984SGNesQNv+ymO3i3/flJuNNrIcSQmWDm44Vo6mUkaCGGKJzk42Zdxb+Z0cmJjpK6VNqd9JECoe5V9dGv8KxfQIyx+ixJfAV4I/KHEeW9pCBKOCAdYwggHSMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUCriv8eHAu/yzi9Pp2awmraA9GgowHwYDVR0jBBgwFoAUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvaXNzdWluZy1jYS5jcmwwdQYIKwYBBQUHAQEEaTBnMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwPAYIKwYBBQUHMAKGMGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9pc3N1aW5nLWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwGQYJKwYBBAGD6F4DBAwGCisGAQQBg+heAwowMwYJKwYBBAGD6F4EBCYMJDAxOWQyMjQxLWViOTctNzcyOC05ZWMwLWNkYWFmYmEzMDBjMjAKBggqhkjOPQQDAwNpADBmAjEA9e9693z4UoLL2Fe9BFkcbglh+CfLRNjlulgE/VPyLVrxgb/FattEgkhMrWIyfQmiAjEA+JtMHY+rwKS/P9o2MbVfSH5tvXcTrXkpPqpaHTVW7g/LK2Pf+dMuRbVlii70549hWQPrMIID5zCCA26gAwIBAgIUT31Fs3EI6FA6i/fOEM+G8cUbSzAwCgYIKoZIzj0EAwMwgY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xIzAhBgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTMxMTEwMzAxNDEyOVowgZExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAkBgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE56GYjZM+Em8LMeptCt79IRV7CTKWC71zG9B2HlQLwm6UNDgSjzBokSvS6l7csnRvixB8ht8nW51dKuAUoPi8RzweUMCPtT28K7TNdeOTDX/kew2Y8PR9Grc1fLc6zAK6o4IBhjCCAYIwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lBBkwFwYKKwYBBAGD6F4CAQYJKoZIhvcvAQEFMB0GA1UdDgQWBBRlDB/oY/6Ws3C27VKnp+CheGDcSTAfBgNVHSMEGDAWgBTobldnKwXMjAtYeHF8tZF037lqHjA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC9yb290LWNhLmNybDByBggrBgEFBQcBAQRmMGQwJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDA5BggrBgEFBQcwAoYtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L3Jvb3QtY2EuY3J0ME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczAKBggqhkjOPQQDAwNnADBkAjBXIrL49fp6vJohGJ94SLlWBs6CFSMSfkACfYbbAdWroF/+WE7e1h/JjdfnGFxbdvwCMC5bizhQibaWWOn9N5JzEoFEKK6PhRMTxwZAk23ZLWvMWae04/5Sjs4wYqfUN26ckKJnc2lnVHN0MqFpdHN0VG9rZW5zgaFjdmFsWQahMIIGnQYJKoZIhvcNAQcCoIIGjjCCBooCAQMxDzANBglghkgBZQMEAgIFADCBiwYLKoZIhvcNAQkQAQSgfAR6MHgCAQEGCisGAQQBg+heBQEwMTANBglghkgBZQMEAgEFAAQgyy32ihJdAK6kjKiVsfnECyayW3qTRi2ysysVxmtqQ6ICFFhj+oFRGYbBW7Mz6x01R+Djl5rRGA8yMDI2MTEwMjAxNDEzMVowAwIBAQIITIqP2m7uhyugggPsMIID6DCCA26gAwIBAgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swCgYIKoZIzj0EAwMwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDIwMTQxMjlaMIGMMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSEwHwYDVQQDDBhFbmN5cGhlciBDMlBBIFRTQSBTaWduZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARr9gYQm+do/W6EIeKN2Uf5q9n8BoHmRqwDR/aTjv7mnsjg5PYBpoehnpzUp6iFBWDe8KAyOTjUx97QD60CJu2FULupHqIFTd5Lr8TD2903S/vL3PdOLM4mqhB/kyoxo4ajggGEMIIBgDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQUMtB7qVGEl2DENzmiThKHM9aMEK8wHwYDVR0jBBgwFoAUGVYfWNddcK6pMhCmHPMntLdClEswTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMHkGCCsGAQUFBwEBBG0wazAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMEAGCCsGAQUFBzAChjRodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvdHNhLWlzc3VpbmctY2EuY3J0MD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3RzYS1pc3N1aW5nLWNhLmNybDAKBggqhkjOPQQDAwNoADBlAjAxltHmUNm63uJrlWppEXbzH8U63vLO/KuiKZh7OyZLWJ73ocBZLrd0IHSxCz3Sln0CMQCYi4S4zwOX0Fg9xQh6/DLMy0O6jInZxIin+OGCbDxdhteAor5E8LFQj/VQwNqkGz0xggH0MIIB8AIBATCBrjCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2AhQoeKpdmqtzWZUbLWXhGy79mL0vizANBglghkgBZQMEAgIFAKCBtjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHgYJKoZIhvcNAQkFMREYDzIwMjYxMTAyMDE0MTMxWjA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCA8afx2houUS/YnXhA2Im3weOs9XV+BouMu8iSEfEywUjA/BgkqhkiG9w0BCQQxMgQwoKj6VeQx687HGSGNPQ3RLT7M3nu+yttSjdkgK3BWl4ZJ5zQsxFKqhaZ65kEWiV1jMAoGCCqGSM49BAMDBGYwZAIwIFtihmnx0IyZvsqBZcpVuEDosVYMk3Wswuen+XBzO7nDTFXCQEwKpiZSF0xil5z7AjAVIfpKNkuxGpcqvX5Ssh7/aCEwBxYv/Jk2mpFglewmGPqqpn5CNlhpDYKJkLywynYaQ1BBZFl5QwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9lhg4DpfNm6/b6faVGyYrF9Mpqg/2Y9gq89VU4siKvXGKGZbRGzNM3ypp4ZPEZ3NJ+NfULxYA/aBbWRRonWDpIFB8bPH/0TgZd5SbzWXHQ8HfJhsxYRaM/2/9F9dkaIolCRx","manifest_sha256":"ccbe864706aa5cfbebefe9429dfcad06edf9f0b4a6a72fb437e8fe3a4281f04b","manifest_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/manifests/external/ref/7de43f3b4c6246dcae7d3a1d6cd32823","xmp_reference":"declined","expected_final_size":205,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_71bfc6826448e889e17e032cd50b2a5c","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"}} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/external-png/source.png b/crates/provenance/encypher/src/fixtures/prepared/external-png/source.png new file mode 100644 index 0000000000..0b577c44c1 Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/external-png/source.png differ diff --git a/crates/provenance/encypher/src/fixtures/prepared/jpeg/envelope.json b/crates/provenance/encypher/src/fixtures/prepared/jpeg/envelope.json new file mode 100644 index 0000000000..12ab10cedb --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/jpeg/envelope.json @@ -0,0 +1,116 @@ +{ + "deployment_id": "async-prepared-api-fixture", + "identity": "8bf97c143a667cb95952df507f0182ada95488ee01f6229581b32609fa3ad9e6", + "item": { + "kind": "embedded", + "output_b64": "/9j/4AAQSkZJRgABAQAAAQABAAD/65GUSlAAAQAAAAEAAJGKanVtYgAAAB5qdW1kYzJwYQARABCAAACqADibcQNjMnBhAAAAkWRqdW1iAAAAR2p1bWRjMm1hABEAEIAAAKoAOJtxA3VybjpjMnBhOmQwYzZlOTYxLWVkYTEtNGE1ZC04YmE4LTYxMTRmNzRmMzM4OQAAAAT/anVtYgAAAClqdW1kYzJhcwARABCAAACqADibcQNjMnBhLmFzc2VydGlvbnMAAAAA6mp1bWIAAAApanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5hY3Rpb25zLnYyAAAAALljYm9yomdhY3Rpb25zgaJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNjYWxnZnNoYTI1NmRoYXNoWCCvdVSXJQ21dNQeBPCpktddD+BZI2pM9fZC8Kz68v5bIHJhbGxBY3Rpb25zSW5jbHVkZWT1AAAAt2p1bWIAAAAtanVtZGNib3IAEQAQgAAAqgA4m3EDb3JnLmVuY3lwaGVyLnN0YXR1cwAAAACCY2JvcqJ0c3RhdHVzTGlzdENyZWRlbnRpYWx4UGh0dHBzOi8vdmVyaWZ5LmVuY3lwaGVyLmNvbS9zdGF0dXMvdjEvbGlzdHMvYmNmZTAwMTQtOTVlYS00YjYxLWJkY2EtODMwMTk1YTQ0Y2Q1b3N0YXR1c0xpc3RJbmRleGExAAAA/2p1bWIAAAAxanVtZGNib3IAEQAQgAAAqgA4m3EDY29tLmVuY3lwaGVyLnByb3ZlbmFuY2UAAAAAxmNib3Klb29yZ2FuaXphdGlvbl9pZHRvcmdfY2RuXzQ4M2RhZjUwZDY0NWlzaWduZXJfaWRtZW5jeXBoZXIuZnJlZWtkb2N1bWVudF9pZHgkZG9jXzgxMjAwZDZlZTUyNTY5MjQ2YzM2Mjk5NTdlMWNlOWEzaGltYWdlX2lkeCRkb2NfODEyMDBkNmVlNTI1NjkyNDZjMzYyOTk1N2UxY2U5YTNpc2lnbmVkX2F0dDIwMjYtMTEtMDJUMDE6NDE6MjlaAAABoGp1bWIAAAAsanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5pbmdyZWRpZW50LnYzAAAAAWxjYm9ypGxyZWxhdGlvbnNoaXBocGFyZW50T2ZoZGM6dGl0bGV4QDU1OWNkNmRlNTY2ZWM5NWQzNjliYWRkZDEwYjliYTBiYjQwODBiNDVjOThiNTQ3MzY2YzVlNzk5NTkyMTlhMTBpZGM6Zm9ybWF0amltYWdlL2pwZWdkZGF0YaVjdXJseJJodHRwczovL2FzeW5jLXByZXBhcmVkLWZpeHR1cmUucHVibGlzaGVyLmV4YW1wbGUuY29tL19lbmN5cGhlci9wcmVwYXJhdGlvbi9zb3VyY2UvNTU5Y2Q2ZGU1NjZlYzk1ZDM2OWJhZGRkMTBiOWJhMGJiNDA4MGI0NWM5OGI1NDczNjZjNWU3OTk1OTIxOWExMGNhbGdmc2hhMjU2ZGhhc2hYIFWc1t5WbsldNput3RC5ugu0CAtFyYtUc2bF55lZIZoQaWRjOmZvcm1hdGppbWFnZS9qcGVnZHNpemUZA/gAAACOanVtYgAAAChqdW1kY2JvcgARABCAAACqADibcQNjMnBhLmhhc2guZGF0YQAAAABeY2JvcqRqZXhjbHVzaW9uc4GiZXN0YXJ0FGZsZW5ndGgZkZZjYWxnZnNoYTI1NmRoYXNoWCBVnNbeVm7JXTabrd0QuboLtAgLRcmLVHNmxeeZWSGaEGNwYWRAAAADiGp1bWIAAAAnanVtZGMyY2wAEQAQgAAAqgA4m3EDYzJwYS5jbGFpbS52MgAAAANZY2JvcqZqaW5zdGFuY2VJRHgtdXJuOnV1aWQ6Yjg1N2Q2ODUtOTg0MC00MGNlLWE0NmItNGY0ZjI1OWUwN2I2dGNsYWltX2dlbmVyYXRvcl9pbmZvo2RuYW1leBtFbmN5cGhlciBDRE4gUHJvdmVuYW5jZS8xLjBndmVyc2lvbmMxLjBrc3BlY1ZlcnNpb25lMi40LjBpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmQwYzZlOTYxLWVkYTEtNGE1ZC04YmE4LTYxMTRmNzRmMzM4OS9jMnBhLnNpZ25hdHVyZXJjcmVhdGVkX2Fzc2VydGlvbnOFo2N1cmx4KnNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuYWN0aW9ucy52MmRoYXNoWCBcaLHL80feE0Z1F2GY+iSIP+kUd4CGVeR8ixGNoxMOcGNhbGdmc2hhMjU2o2N1cmx4LnNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL29yZy5lbmN5cGhlci5zdGF0dXNkaGFzaFgg9AlcABWOC6oB3Vu7MJVFTNwOuN48Nc/q3hNhNxf9/6VjYWxnZnNoYTI1NqNjdXJseDJzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jb20uZW5jeXBoZXIucHJvdmVuYW5jZWRoYXNoWCDLLZNiH4t4aRKmpf/Mfqm2XFXLnFC90Z/fdQQVxak8w2NhbGdmc2hhMjU2o2N1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCvdVSXJQ21dNQeBPCpktddD+BZI2pM9fZC8Kz68v5bIGNhbGdmc2hhMjU2o2N1cmx4KXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaGFzaC5kYXRhZGhhc2hYIAPd8fnhgNj3PEVx8aHuqyrnjLQRlENCUxD5nFgHhtW7Y2FsZ2ZzaGEyNTZjYWxnZnNoYTI1NmhkYzp0aXRsZXhANTU5Y2Q2ZGU1NjZlYzk1ZDM2OWJhZGRkMTBiOWJhMGJiNDA4MGI0NWM5OGI1NDczNjZjNWU3OTk1OTIxOWExMAAAiI5qdW1iAAAAKGp1bWRjMmNzABEAEIAAAKoAOJtxA2MycGEuc2lnbmF0dXJlAAAAiF5jYm9y0oRZB+WiATgiGCGCWQPtMIID6TCCA26gAwIBAgIUGX8Yv3cXfmzZcGD3B8bKBCbPVWowCgYIKoZIzj0EAwMwgZExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAkBgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMzAxNDEyOVowPzELMAkGA1UEBhMCVVMxHTAbBgNVBAoMFEVuY3lwaGVyIENvcnBvcmF0aW9uMREwDwYDVQQDDAhFbmN5cGhlcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABDvZ7+eBfyDr2C485geb984SGNesQNv+ymO3i3/flJuNNrIcSQmWDm44Vo6mUkaCGGKJzk42Zdxb+Z0cmJjpK6VNqd9JECoe5V9dGv8KxfQIyx+ixJfAV4I/KHEeW9pCBKOCAdYwggHSMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUCriv8eHAu/yzi9Pp2awmraA9GgowHwYDVR0jBBgwFoAUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvaXNzdWluZy1jYS5jcmwwdQYIKwYBBQUHAQEEaTBnMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwPAYIKwYBBQUHMAKGMGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9pc3N1aW5nLWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwGQYJKwYBBAGD6F4DBAwGCisGAQQBg+heAwowMwYJKwYBBAGD6F4EBCYMJDAxOWQyMjQxLWViOTctNzcyOC05ZWMwLWNkYWFmYmEzMDBjMjAKBggqhkjOPQQDAwNpADBmAjEA9e9693z4UoLL2Fe9BFkcbglh+CfLRNjlulgE/VPyLVrxgb/FattEgkhMrWIyfQmiAjEA+JtMHY+rwKS/P9o2MbVfSH5tvXcTrXkpPqpaHTVW7g/LK2Pf+dMuRbVlii70549hWQPrMIID5zCCA26gAwIBAgIUT31Fs3EI6FA6i/fOEM+G8cUbSzAwCgYIKoZIzj0EAwMwgY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xIzAhBgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTMxMTEwMzAxNDEyOVowgZExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAkBgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE56GYjZM+Em8LMeptCt79IRV7CTKWC71zG9B2HlQLwm6UNDgSjzBokSvS6l7csnRvixB8ht8nW51dKuAUoPi8RzweUMCPtT28K7TNdeOTDX/kew2Y8PR9Grc1fLc6zAK6o4IBhjCCAYIwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lBBkwFwYKKwYBBAGD6F4CAQYJKoZIhvcvAQEFMB0GA1UdDgQWBBRlDB/oY/6Ws3C27VKnp+CheGDcSTAfBgNVHSMEGDAWgBTobldnKwXMjAtYeHF8tZF037lqHjA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC9yb290LWNhLmNybDByBggrBgEFBQcBAQRmMGQwJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDA5BggrBgEFBQcwAoYtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L3Jvb3QtY2EuY3J0ME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczAKBggqhkjOPQQDAwNnADBkAjBXIrL49fp6vJohGJ94SLlWBs6CFSMSfkACfYbbAdWroF/+WE7e1h/JjdfnGFxbdvwCMC5bizhQibaWWOn9N5JzEoFEKK6PhRMTxwZAk23ZLWvMWae04/5Sjs4wYqfUN26ckKJnc2lnVHN0MqFpdHN0VG9rZW5zgaFjdmFsWQaiMIIGngYJKoZIhvcNAQcCoIIGjzCCBosCAQMxDzANBglghkgBZQMEAgIFADCBiwYLKoZIhvcNAQkQAQSgfAR6MHgCAQEGCisGAQQBg+heBQEwMTANBglghkgBZQMEAgEFAAQgwIwpKGcDJgE9SWLBPFmuhNcDgj+Uy5as5rrc9ESfl54CFEZ3OzMgHwmOyZmivK+8x/B93sv5GA8yMDI2MTEwMjAxNDEyOVowAwIBAQIIPOmS45824X+gggPsMIID6DCCA26gAwIBAgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swCgYIKoZIzj0EAwMwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDIwMTQxMjlaMIGMMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSEwHwYDVQQDDBhFbmN5cGhlciBDMlBBIFRTQSBTaWduZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARr9gYQm+do/W6EIeKN2Uf5q9n8BoHmRqwDR/aTjv7mnsjg5PYBpoehnpzUp6iFBWDe8KAyOTjUx97QD60CJu2FULupHqIFTd5Lr8TD2903S/vL3PdOLM4mqhB/kyoxo4ajggGEMIIBgDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQUMtB7qVGEl2DENzmiThKHM9aMEK8wHwYDVR0jBBgwFoAUGVYfWNddcK6pMhCmHPMntLdClEswTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMHkGCCsGAQUFBwEBBG0wazAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMEAGCCsGAQUFBzAChjRodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvdHNhLWlzc3VpbmctY2EuY3J0MD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3RzYS1pc3N1aW5nLWNhLmNybDAKBggqhkjOPQQDAwNoADBlAjAxltHmUNm63uJrlWppEXbzH8U63vLO/KuiKZh7OyZLWJ73ocBZLrd0IHSxCz3Sln0CMQCYi4S4zwOX0Fg9xQh6/DLMy0O6jInZxIin+OGCbDxdhteAor5E8LFQj/VQwNqkGz0xggH1MIIB8QIBATCBrjCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2AhQoeKpdmqtzWZUbLWXhGy79mL0vizANBglghkgBZQMEAgIFAKCBtjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHgYJKoZIhvcNAQkFMREYDzIwMjYxMTAyMDE0MTI5WjA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCA8afx2houUS/YnXhA2Im3weOs9XV+BouMu8iSEfEywUjA/BgkqhkiG9w0BCQQxMgQw35gYGhsBz5X7+GLDH4OWRcyEbD5aQe4hecSXkyVqYVIuBcdDD03lnaaFdCqw27xeMAoGCCqGSM49BAMDBGcwZQIxAIEOa55cVuosmLHFXvzKl9URemw//v46q86JPEXPTggYs5i1itY01nfnMuPYEnqTJwIwNkgjb7V2VuSzg40C7YNzpJc7h8u/ZvADac6wJv4cSD4xGquKnHL6a/YEep3SF3YBGkNQQWRZeUIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9lhgj1PRGmqNeDq6c07+H6mXlVb1hEoKn7BbeIPki016pyj5XGzFApsOSaxjNDs+Pp+GGPQCZDk0nw0stQmQ55mz45hTzj3UUzu4U7pNFOnUkLPJ9W6r0PE7AXBocxhcEPZN/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAIAAwAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8fs7DpxW/Z6f04q1Z2HTit+z0/pxXXl+J2MoVinb2RSJmC8hSeals7DpxXRR2IW3ckfwkVPZ2HTisOIcZevShfZN/e/+AerhK2jKtnp/TitpbNIrSV5CFUIcsegq/Z6f04qDxOxg002cY+eZQXPIIXI/nz/k0qWIk8NU5d1GT+5M6cVmUMHRdefT8X0Rx9nYdOK37PT+nFWrOw6cVv2en9OK8bL8TsfFwrGcdP8A9Ebj0/nVqzsOnFaGqTWGlWSyX13DbKSSBI4BcAZOB1btwM9qwpvH+jWq4sYpr6TaCCFMaZzyCWGQcc/dPb8ObN5V8Tj4qlFu0UvLdvfbqfRZbh8TiI/uoN3+779jplhjs7V7iUHYgycDJ9hXJXkElyWnk+9JJk+n0+lcZ4s+J+talefYtNSCytoWDEqolctjBBZuCOeyg54ridVuL/VDG1/dTXDb2dfMckIT12joB04GOlfU0cmrU8rqVK0lG69XrZL7/U8TN8Dicbi40JTUYQ+b5uvlptv3P//Z", + "planned": { + "digest": "559cd6de566ec95d369baddd10b9ba0bb4080b45c98b547366c5e79959219a10", + "geometry": { + "binding_recipe": { + "algorithm": "sha256", + "exclusions": [ + { + "length": 37270, + "offset": 20 + } + ], + "type": "data_hash" + }, + "carrier_output_span": [ + 20, + 37270 + ], + "carrier_recipe": { + "type": "standard" + }, + "carrier_span": [ + 0, + 37270 + ], + "disposition": "embedded", + "edits": [ + { + "length": 37270, + "offset": 20, + "op": "insert_carrier" + } + ], + "expected_final_size": 38286, + "planner_options": { + "tiff_placement": "main_ifd_when_single" + } + }, + "idempotency_key": "async-prepared-fixture:jpeg:c2e4fc97bb654e2482182f084ef2b26b", + "plan_id": "7ee687ad-4b19-4541-9260-3f8ce5c3c011", + "release": { + "abi": 1, + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "release": 18, + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f", + "signature": "33Dab6p1n5yJpIbXypes+yr6xXalcFonsh5h9YaFzMo18zFMHlNDXzriZNOe5LHZQ03P7Ki0T4ZXT+QNS6okCw==" + }, + "request": { + "carrier_requirement": "embedded_required", + "contract_version": 2, + "expected_policy_digest": "171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff", + "page_url": "https://async-prepared-fixture.publisher.example.com/article", + "probe": { + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f" + }, + "protocol": "encypher-prepared-signing/2", + "source": { + "byte_length": 1016, + "mime": "image/jpeg", + "origin_host": "async-prepared-fixture.publisher.example.com", + "reference": "https://async-prepared-fixture.publisher.example.com/_encypher/preparation/source/559cd6de566ec95d369baddd10b9ba0bb4080b45c98b547366c5e79959219a10", + "reference_kind": "publisher_snapshot", + "sha256": "559cd6de566ec95d369baddd10b9ba0bb4080b45c98b547366c5e79959219a10" + }, + "structure": [ + { + "b64": "/9j/4AAQ", + "offset": 0 + }, + { + "b64": "/9sAQw==", + "offset": 20 + }, + { + "b64": "/9sAQw==", + "offset": 89 + }, + { + "b64": "/8AAEQ==", + "offset": 158 + }, + { + "b64": "/8QAHw==", + "offset": 177 + }, + { + "b64": "/8QAtQ==", + "offset": 210 + }, + { + "b64": "/8QAHw==", + "offset": 393 + }, + { + "b64": "/8QAtQ==", + "offset": 426 + }, + { + "b64": "/9o=", + "offset": 609 + } + ] + } + }, + "source_b64": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAgADADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDx+zsOnFb9np/TirVnYdOK37PT+nFdeX4nYyhWKdvZFImYLyFJ5qWzsOnFdFHYhbdyR/CRU9nYdOKw4hxl69KF9k397/4B6uEraMq2en9OK2ls0itJXkIVQhyx6Cr9np/TioPE7GDTTZxj55lBc8ghcj+fP+TSpYiTw1Tl3UZP7kzpxWZQwdF159PxfRHH2dh04rfs9P6cVas7DpxW/Z6f04rxsvxOx8XCsZx0/wD0RuPT+dWrOw6cVoapNYaVZLJfXcNspJIEjgFwBk4HVu3Az2rCm8f6NariximvpNoIIUxpnPIJYZBxz909vw5s3lXxOPiqUW7RS8t299up9FluHxOIj+6g3f7vv2OmWGOztXuJQdiDJwMn2FcleQSXJaeT70kmT6fT6Vxniz4n61qV59i01ILK2hYMSqiVy2MEFm4I57KDniuJ1W4v9UMbX91NcNvZ18xyQhPXaOgHTgY6V9TRyatTyupUrSUbr1etkvv9TxM3wOJxuLjQlNRhD5vm6+Wm2/c//9k=" + }, + "protocol": "encypher-preparation/1", + "retirement_epoch": 0, + "variant": "" +} diff --git a/crates/provenance/encypher/src/fixtures/prepared/jpeg/finalize.json b/crates/provenance/encypher/src/fixtures/prepared/jpeg/finalize.json new file mode 100644 index 0000000000..0c2d8562ae --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/jpeg/finalize.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"7ee687ad-4b19-4541-9260-3f8ce5c3c011","evidence_id":"5ca6f7a8-519b-46d8-9010-d80c07ff3911","instance_id":"urn:uuid:b857d685-9840-40ce-a46b-4f4f259e07b6","document_id":"doc_81200d6ee52569246c3629957e1ce9a3","asset_id":"doc_81200d6ee52569246c3629957e1ce9a3","mime":"image/jpeg","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"559cd6de566ec95d369baddd10b9ba0bb4080b45c98b547366c5e79959219a10","binding":{"type":"data_hash","digest":"559cd6de566ec95d369baddd10b9ba0bb4080b45c98b547366c5e79959219a10"},"manifest_store_b64":"AACRimp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFkanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpkMGM2ZTk2MS1lZGExLTRhNWQtOGJhOC02MTE0Zjc0ZjMzODkAAAAE/2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFggr3VUlyUNtXTUHgTwqZLXXQ/gWSNqTPX2QvCs+vL+WyByYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhMQAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY184MTIwMGQ2ZWU1MjU2OTI0NmMzNjI5OTU3ZTFjZTlhM2hpbWFnZV9pZHgkZG9jXzgxMjAwZDZlZTUyNTY5MjQ2YzM2Mjk5NTdlMWNlOWEzaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjI5WgAAAaBqdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFsY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEA1NTljZDZkZTU2NmVjOTVkMzY5YmFkZGQxMGI5YmEwYmI0MDgwYjQ1Yzk4YjU0NzM2NmM1ZTc5OTU5MjE5YTEwaWRjOmZvcm1hdGppbWFnZS9qcGVnZGRhdGGlY3VybHiSaHR0cHM6Ly9hc3luYy1wcmVwYXJlZC1maXh0dXJlLnB1Ymxpc2hlci5leGFtcGxlLmNvbS9fZW5jeXBoZXIvcHJlcGFyYXRpb24vc291cmNlLzU1OWNkNmRlNTY2ZWM5NWQzNjliYWRkZDEwYjliYTBiYjQwODBiNDVjOThiNTQ3MzY2YzVlNzk5NTkyMTlhMTBjYWxnZnNoYTI1NmRoYXNoWCBVnNbeVm7JXTabrd0QuboLtAgLRcmLVHNmxeeZWSGaEGlkYzpmb3JtYXRqaW1hZ2UvanBlZ2RzaXplGQP4AAAAjmp1bWIAAAAoanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5oYXNoLmRhdGEAAAAAXmNib3KkamV4Y2x1c2lvbnOBomVzdGFydBRmbGVuZ3RoGZGWY2FsZ2ZzaGEyNTZkaGFzaFggVZzW3lZuyV02m63dELm6C7QIC0XJi1RzZsXnmVkhmhBjcGFkQAAAA4hqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADWWNib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOmI4NTdkNjg1LTk4NDAtNDBjZS1hNDZiLTRmNGYyNTllMDdiNnRjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYTpkMGM2ZTk2MS1lZGExLTRhNWQtOGJhOC02MTE0Zjc0ZjMzODkvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggXGixy/NH3hNGdRdhmPokiD/pFHeAhlXkfIsRjaMTDnBjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIPQJXAAVjguqAd1buzCVRUzcDrjePDXP6t4TYTcX/f+lY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFggyy2TYh+LeGkSpqX/zH6ptlxVy5xQvdGf33UEFcWpPMNjYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggr3VUlyUNtXTUHgTwqZLXXQ/gWSNqTPX2QvCs+vL+WyBjYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCAD3fH54YDY9zxFcfGh7qsq54y0EZRDQlMQ+ZxYB4bVu2NhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGV4QDU1OWNkNmRlNTY2ZWM5NWQzNjliYWRkZDEwYjliYTBiYjQwODBiNDVjOThiNTQ3MzY2YzVlNzk5NTkyMTlhMTAAAIiOanVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAAIheY2JvctKEWQflogE4IhghglkD7TCCA+kwggNuoAMCAQICFBl/GL93F35s2XBg9wfGygQmz1VqMAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDMwMTQxMjlaMD8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKDBRFbmN5cGhlciBDb3Jwb3JhdGlvbjERMA8GA1UEAwwIRW5jeXBoZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ72e/ngX8g69guPOYHm/fOEhjXrEDb/spjt4t/35SbjTayHEkJlg5uOFaOplJGghhiic5ONmXcW/mdHJiY6SulTanfSRAqHuVfXRr/CsX0CMsfosSXwFeCPyhxHlvaQgSjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFAq4r/HhwLv8s4vT6dmsJq2gPRoKMB8GA1UdIwQYMBaAFGUMH+hj/pazcLbtUqen4KF4YNxJMDoGA1UdHwQzMDEwL6AtoCuGKWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL2lzc3VpbmctY2EuY3JsMHUGCCsGAQUFBwEBBGkwZzAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDwGCCsGAQUFBzAChjBodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvaXNzdWluZy1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMBkGCSsGAQQBg+heAwQMBgorBgEEAYPoXgMKMDMGCSsGAQQBg+heBAQmDCQwMTlkMjI0MS1lYjk3LTc3MjgtOWVjMC1jZGFhZmJhMzAwYzIwCgYIKoZIzj0EAwMDaQAwZgIxAPXvevd8+FKCy9hXvQRZHG4JYfgny0TY5bpYBP1T8i1a8YG/xWrbRIJITK1iMn0JogIxAPibTB2Pq8Ckvz/aNjG1X0h+bb13E615KT6qWh01Vu4Pyytj3/nTLkW1ZYou9OePYVkD6zCCA+cwggNuoAMCAQICFE99RbNxCOhQOov3zhDPhvHFG0swMAoGCCqGSM49BAMDMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSMwIQYDVQQDDBpFbmN5cGhlciBDMlBBIFJvb3QgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0zMTExMDMwMTQxMjlaMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOehmI2TPhJvCzHqbQre/SEVewkylgu9cxvQdh5UC8JulDQ4Eo8waJEr0upe3LJ0b4sQfIbfJ1udXSrgFKD4vEc8HlDAj7U9vCu0zXXjkw1/5HsNmPD0fRq3NXy3OswCuqOCAYYwggGCMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwHwYDVR0jBBgwFoAU6G5XZysFzIwLWHhxfLWRdN+5ah4wNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvcm9vdC1jYS5jcmwwcgYIKwYBBQUHAQEEZjBkMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwOQYIKwYBBQUHMAKGLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9yb290LWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwCgYIKoZIzj0EAwMDZwAwZAIwVyKy+PX6eryaIRifeEi5VgbOghUjEn5AAn2G2wHVq6Bf/lhO3tYfyY3X5xhcW3b8AjAuW4s4UIm2lljp/TeScxKBRCiuj4UTE8cGQJNt2S1rzFmntOP+Uo7OMGKn1DdunJCiZ3NpZ1RzdDKhaXRzdFRva2Vuc4GhY3ZhbFkGojCCBp4GCSqGSIb3DQEHAqCCBo8wggaLAgEDMQ8wDQYJYIZIAWUDBAICBQAwgYsGCyqGSIb3DQEJEAEEoHwEejB4AgEBBgorBgEEAYPoXgUBMDEwDQYJYIZIAWUDBAIBBQAEIMCMKShnAyYBPUliwTxZroTXA4I/lMuWrOa63PREn5eeAhRGdzszIB8JjsmZoryvvMfwfd7L+RgPMjAyNjExMDIwMTQxMjlaMAMCAQECCDzpkuOfNuF/oIID7DCCA+gwggNuoAMCAQICFCh4ql2aq3NZlRstZeEbLv2YvS+LMAoGCCqGSM49BAMDMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSowKAYDVQQDDCFFbmN5cGhlciBDMlBBIFRTQSBJc3N1aW5nIENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMjcxMTAyMDE0MTI5WjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEhMB8GA1UEAwwYRW5jeXBoZXIgQzJQQSBUU0EgU2lnbmVyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEa/YGEJvnaP1uhCHijdlH+avZ/AaB5kasA0f2k47+5p7I4OT2AaaHoZ6c1KeohQVg3vCgMjk41Mfe0A+tAibthVC7qR6iBU3eS6/Ew9vdN0v7y9z3TizOJqoQf5MqMaOGo4IBhDCCAYAwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwHQYDVR0OBBYEFDLQe6lRhJdgxDc5ok4ShzPWjBCvMB8GA1UdIwQYMBaAFBlWH1jXXXCuqTIQphzzJ7S3QpRLME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczB5BggrBgEFBQcBAQRtMGswJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDBABggrBgEFBQcwAoY0aHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L3RzYS1pc3N1aW5nLWNhLmNydDA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC90c2EtaXNzdWluZy1jYS5jcmwwCgYIKoZIzj0EAwMDaAAwZQIwMZbR5lDZut7ia5VqaRF28x/FOt7yzvyroimYezsmS1ie96HAWS63dCB0sQs90pZ9AjEAmIuEuM8Dl9BYPcUIevwyzMtDuoyJ2cSIp/jhgmw8XYbXgKK+RPCxUI/1UMDapBs9MYIB9TCCAfECAQEwga4wgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swDQYJYIZIAWUDBAICBQCggbYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMB4GCSqGSIb3DQEJBTERGA8yMDI2MTEwMjAxNDEyOVowNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgPGn8doaLlEv2J14QNiJt8HjrPV1fgaLjLvIkhHxMsFIwPwYJKoZIhvcNAQkEMTIEMN+YGBobAc+V+/hiwx+DlkXMhGw+WkHuIXnEl5MlamFSLgXHQw9N5Z2mhXQqsNu8XjAKBggqhkjOPQQDAwRnMGUCMQCBDmueXFbqLJixxV78ypfVEXpsP/7+OqvOiTxFz04IGLOYtYrWNNZ35zLj2BJ6kycCMDZII2+1dlbks4ONAu2Dc6SXO4fLv2bwA2nOsCb+HEg+MRqripxy+mv2BHqd0hd2ARpDUEFkWXlCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYYI9T0RpqjXg6unNO/h+pl5VW9YRKCp+wW3iD5ItNeqco+VxsxQKbDkmsYzQ7Pj6fhhj0AmQ5NJ8NLLUJkOeZs+OYU8491FM7uFO6TRTp1JCzyfVuq9DxOwFwaHMYXBD2TQ==","manifest_sha256":"0698a056558265983b164ae31631cc196f76e9b1b2fa513d74fa2c787def256d","carrier_sha256":"c6e10ee108720a238fd925b7bd8c206ac0d8788245b5430a250afb8d32a64df8","expected_final_size":38286,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_81200d6ee52569246c3629957e1ce9a3","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/jpeg/output.jpg b/crates/provenance/encypher/src/fixtures/prepared/jpeg/output.jpg new file mode 100644 index 0000000000..915e68d67c Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/jpeg/output.jpg differ diff --git a/crates/provenance/encypher/src/fixtures/prepared/jpeg/prepare.json b/crates/provenance/encypher/src/fixtures/prepared/jpeg/prepare.json new file mode 100644 index 0000000000..1456cea81a --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/jpeg/prepare.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"7ee687ad-4b19-4541-9260-3f8ce5c3c011","instance_id":"urn:uuid:b857d685-9840-40ce-a46b-4f4f259e07b6","document_id":"doc_81200d6ee52569246c3629957e1ce9a3","expires_at":"2026-11-02T02:41:29.639419Z","mime":"image/jpeg","expected_manifest_size":37258,"effective_action":"c2pa.opened","geometry":{"disposition":"embedded","edits":[{"op":"insert_carrier","offset":20,"length":37270}],"carrier_span":[0,37270],"carrier_output_span":[20,37270],"expected_final_size":38286,"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[{"offset":20,"length":37270}]},"carrier_recipe":{"type":"standard"},"planner_options":{"tiff_placement":"main_ifd_when_single"}},"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/jpeg/recover.json b/crates/provenance/encypher/src/fixtures/prepared/jpeg/recover.json new file mode 100644 index 0000000000..cb2af2e9a5 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/jpeg/recover.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff","prepare":{"protocol":"encypher-prepared-signing/2","plan_id":"7ee687ad-4b19-4541-9260-3f8ce5c3c011","instance_id":"urn:uuid:b857d685-9840-40ce-a46b-4f4f259e07b6","document_id":"doc_81200d6ee52569246c3629957e1ce9a3","expires_at":"2026-11-02T02:41:29.639419Z","mime":"image/jpeg","expected_manifest_size":37258,"effective_action":"c2pa.opened","geometry":{"edits":[{"op":"insert_carrier","length":37270,"offset":20}],"disposition":"embedded","carrier_span":[0,37270],"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[{"length":37270,"offset":20}]},"carrier_recipe":{"type":"standard"},"planner_options":{"tiff_placement":"main_ifd_when_single"},"carrier_output_span":[20,37270],"expected_final_size":38286},"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"},"finalize":{"protocol":"encypher-prepared-signing/2","plan_id":"7ee687ad-4b19-4541-9260-3f8ce5c3c011","evidence_id":"5ca6f7a8-519b-46d8-9010-d80c07ff3911","instance_id":"urn:uuid:b857d685-9840-40ce-a46b-4f4f259e07b6","document_id":"doc_81200d6ee52569246c3629957e1ce9a3","asset_id":"doc_81200d6ee52569246c3629957e1ce9a3","mime":"image/jpeg","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"559cd6de566ec95d369baddd10b9ba0bb4080b45c98b547366c5e79959219a10","binding":{"type":"data_hash","digest":"559cd6de566ec95d369baddd10b9ba0bb4080b45c98b547366c5e79959219a10"},"manifest_store_b64":"AACRimp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFkanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpkMGM2ZTk2MS1lZGExLTRhNWQtOGJhOC02MTE0Zjc0ZjMzODkAAAAE/2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFggr3VUlyUNtXTUHgTwqZLXXQ/gWSNqTPX2QvCs+vL+WyByYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhMQAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY184MTIwMGQ2ZWU1MjU2OTI0NmMzNjI5OTU3ZTFjZTlhM2hpbWFnZV9pZHgkZG9jXzgxMjAwZDZlZTUyNTY5MjQ2YzM2Mjk5NTdlMWNlOWEzaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjI5WgAAAaBqdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFsY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEA1NTljZDZkZTU2NmVjOTVkMzY5YmFkZGQxMGI5YmEwYmI0MDgwYjQ1Yzk4YjU0NzM2NmM1ZTc5OTU5MjE5YTEwaWRjOmZvcm1hdGppbWFnZS9qcGVnZGRhdGGlY3VybHiSaHR0cHM6Ly9hc3luYy1wcmVwYXJlZC1maXh0dXJlLnB1Ymxpc2hlci5leGFtcGxlLmNvbS9fZW5jeXBoZXIvcHJlcGFyYXRpb24vc291cmNlLzU1OWNkNmRlNTY2ZWM5NWQzNjliYWRkZDEwYjliYTBiYjQwODBiNDVjOThiNTQ3MzY2YzVlNzk5NTkyMTlhMTBjYWxnZnNoYTI1NmRoYXNoWCBVnNbeVm7JXTabrd0QuboLtAgLRcmLVHNmxeeZWSGaEGlkYzpmb3JtYXRqaW1hZ2UvanBlZ2RzaXplGQP4AAAAjmp1bWIAAAAoanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5oYXNoLmRhdGEAAAAAXmNib3KkamV4Y2x1c2lvbnOBomVzdGFydBRmbGVuZ3RoGZGWY2FsZ2ZzaGEyNTZkaGFzaFggVZzW3lZuyV02m63dELm6C7QIC0XJi1RzZsXnmVkhmhBjcGFkQAAAA4hqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADWWNib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOmI4NTdkNjg1LTk4NDAtNDBjZS1hNDZiLTRmNGYyNTllMDdiNnRjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYTpkMGM2ZTk2MS1lZGExLTRhNWQtOGJhOC02MTE0Zjc0ZjMzODkvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggXGixy/NH3hNGdRdhmPokiD/pFHeAhlXkfIsRjaMTDnBjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIPQJXAAVjguqAd1buzCVRUzcDrjePDXP6t4TYTcX/f+lY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFggyy2TYh+LeGkSpqX/zH6ptlxVy5xQvdGf33UEFcWpPMNjYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggr3VUlyUNtXTUHgTwqZLXXQ/gWSNqTPX2QvCs+vL+WyBjYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCAD3fH54YDY9zxFcfGh7qsq54y0EZRDQlMQ+ZxYB4bVu2NhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGV4QDU1OWNkNmRlNTY2ZWM5NWQzNjliYWRkZDEwYjliYTBiYjQwODBiNDVjOThiNTQ3MzY2YzVlNzk5NTkyMTlhMTAAAIiOanVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAAIheY2JvctKEWQflogE4IhghglkD7TCCA+kwggNuoAMCAQICFBl/GL93F35s2XBg9wfGygQmz1VqMAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDMwMTQxMjlaMD8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKDBRFbmN5cGhlciBDb3Jwb3JhdGlvbjERMA8GA1UEAwwIRW5jeXBoZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ72e/ngX8g69guPOYHm/fOEhjXrEDb/spjt4t/35SbjTayHEkJlg5uOFaOplJGghhiic5ONmXcW/mdHJiY6SulTanfSRAqHuVfXRr/CsX0CMsfosSXwFeCPyhxHlvaQgSjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFAq4r/HhwLv8s4vT6dmsJq2gPRoKMB8GA1UdIwQYMBaAFGUMH+hj/pazcLbtUqen4KF4YNxJMDoGA1UdHwQzMDEwL6AtoCuGKWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL2lzc3VpbmctY2EuY3JsMHUGCCsGAQUFBwEBBGkwZzAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDwGCCsGAQUFBzAChjBodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvaXNzdWluZy1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMBkGCSsGAQQBg+heAwQMBgorBgEEAYPoXgMKMDMGCSsGAQQBg+heBAQmDCQwMTlkMjI0MS1lYjk3LTc3MjgtOWVjMC1jZGFhZmJhMzAwYzIwCgYIKoZIzj0EAwMDaQAwZgIxAPXvevd8+FKCy9hXvQRZHG4JYfgny0TY5bpYBP1T8i1a8YG/xWrbRIJITK1iMn0JogIxAPibTB2Pq8Ckvz/aNjG1X0h+bb13E615KT6qWh01Vu4Pyytj3/nTLkW1ZYou9OePYVkD6zCCA+cwggNuoAMCAQICFE99RbNxCOhQOov3zhDPhvHFG0swMAoGCCqGSM49BAMDMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSMwIQYDVQQDDBpFbmN5cGhlciBDMlBBIFJvb3QgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0zMTExMDMwMTQxMjlaMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOehmI2TPhJvCzHqbQre/SEVewkylgu9cxvQdh5UC8JulDQ4Eo8waJEr0upe3LJ0b4sQfIbfJ1udXSrgFKD4vEc8HlDAj7U9vCu0zXXjkw1/5HsNmPD0fRq3NXy3OswCuqOCAYYwggGCMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwHwYDVR0jBBgwFoAU6G5XZysFzIwLWHhxfLWRdN+5ah4wNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvcm9vdC1jYS5jcmwwcgYIKwYBBQUHAQEEZjBkMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwOQYIKwYBBQUHMAKGLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9yb290LWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwCgYIKoZIzj0EAwMDZwAwZAIwVyKy+PX6eryaIRifeEi5VgbOghUjEn5AAn2G2wHVq6Bf/lhO3tYfyY3X5xhcW3b8AjAuW4s4UIm2lljp/TeScxKBRCiuj4UTE8cGQJNt2S1rzFmntOP+Uo7OMGKn1DdunJCiZ3NpZ1RzdDKhaXRzdFRva2Vuc4GhY3ZhbFkGojCCBp4GCSqGSIb3DQEHAqCCBo8wggaLAgEDMQ8wDQYJYIZIAWUDBAICBQAwgYsGCyqGSIb3DQEJEAEEoHwEejB4AgEBBgorBgEEAYPoXgUBMDEwDQYJYIZIAWUDBAIBBQAEIMCMKShnAyYBPUliwTxZroTXA4I/lMuWrOa63PREn5eeAhRGdzszIB8JjsmZoryvvMfwfd7L+RgPMjAyNjExMDIwMTQxMjlaMAMCAQECCDzpkuOfNuF/oIID7DCCA+gwggNuoAMCAQICFCh4ql2aq3NZlRstZeEbLv2YvS+LMAoGCCqGSM49BAMDMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSowKAYDVQQDDCFFbmN5cGhlciBDMlBBIFRTQSBJc3N1aW5nIENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMjcxMTAyMDE0MTI5WjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEhMB8GA1UEAwwYRW5jeXBoZXIgQzJQQSBUU0EgU2lnbmVyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEa/YGEJvnaP1uhCHijdlH+avZ/AaB5kasA0f2k47+5p7I4OT2AaaHoZ6c1KeohQVg3vCgMjk41Mfe0A+tAibthVC7qR6iBU3eS6/Ew9vdN0v7y9z3TizOJqoQf5MqMaOGo4IBhDCCAYAwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwHQYDVR0OBBYEFDLQe6lRhJdgxDc5ok4ShzPWjBCvMB8GA1UdIwQYMBaAFBlWH1jXXXCuqTIQphzzJ7S3QpRLME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczB5BggrBgEFBQcBAQRtMGswJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDBABggrBgEFBQcwAoY0aHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L3RzYS1pc3N1aW5nLWNhLmNydDA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC90c2EtaXNzdWluZy1jYS5jcmwwCgYIKoZIzj0EAwMDaAAwZQIwMZbR5lDZut7ia5VqaRF28x/FOt7yzvyroimYezsmS1ie96HAWS63dCB0sQs90pZ9AjEAmIuEuM8Dl9BYPcUIevwyzMtDuoyJ2cSIp/jhgmw8XYbXgKK+RPCxUI/1UMDapBs9MYIB9TCCAfECAQEwga4wgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swDQYJYIZIAWUDBAICBQCggbYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMB4GCSqGSIb3DQEJBTERGA8yMDI2MTEwMjAxNDEyOVowNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgPGn8doaLlEv2J14QNiJt8HjrPV1fgaLjLvIkhHxMsFIwPwYJKoZIhvcNAQkEMTIEMN+YGBobAc+V+/hiwx+DlkXMhGw+WkHuIXnEl5MlamFSLgXHQw9N5Z2mhXQqsNu8XjAKBggqhkjOPQQDAwRnMGUCMQCBDmueXFbqLJixxV78ypfVEXpsP/7+OqvOiTxFz04IGLOYtYrWNNZ35zLj2BJ6kycCMDZII2+1dlbks4ONAu2Dc6SXO4fLv2bwA2nOsCb+HEg+MRqripxy+mv2BHqd0hd2ARpDUEFkWXlCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYYI9T0RpqjXg6unNO/h+pl5VW9YRKCp+wW3iD5ItNeqco+VxsxQKbDkmsYzQ7Pj6fhhj0AmQ5NJ8NLLUJkOeZs+OYU8491FM7uFO6TRTp1JCzyfVuq9DxOwFwaHMYXBD2TQ==","manifest_sha256":"0698a056558265983b164ae31631cc196f76e9b1b2fa513d74fa2c787def256d","carrier_sha256":"c6e10ee108720a238fd925b7bd8c206ac0d8788245b5430a250afb8d32a64df8","expected_final_size":38286,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_81200d6ee52569246c3629957e1ce9a3","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"}} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/jpeg/source.jpg b/crates/provenance/encypher/src/fixtures/prepared/jpeg/source.jpg new file mode 100644 index 0000000000..914f437753 Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/jpeg/source.jpg differ diff --git a/crates/provenance/encypher/src/fixtures/prepared/png/envelope.json b/crates/provenance/encypher/src/fixtures/prepared/png/envelope.json new file mode 100644 index 0000000000..37e7699acc --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/png/envelope.json @@ -0,0 +1,92 @@ +{ + "deployment_id": "async-prepared-api-fixture", + "identity": "3e437f891904800fd0f2eb2604ca6cdff29261e0b2b5fb3be888cd1cdf4b6a05", + "item": { + "kind": "embedded", + "output_b64": "iVBORw0KGgoAAAANSUhEUgAAADAAAAAgCAIAAADbtmxLAAAAlElEQVR4nO3XywqAIBCFYS8dWsz7v29bJS+Bo3MSYRbRYvz4KSLvnEMQnrni7WMgmp1BwgaiK7Qn6LxlPwOBDcRVCFQgEBbSBQ19N5Bcb1tIJ489KNeIPeg9liCUbp5CzTxmoJrGshARqJFHCzT8v5EYVhfK8pSOXgpCBWEB6p8i80H55vazPLPQ2EIlUG/Jxzwx+AdPEifC6qgrjgAAkYhjYUJYAACRiGp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFianVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTphMjU3ZTg1Yi0wZWU5LTQ0YTgtOGQxMS0xYjgxNGY2M2RkMTYAAAAE/Wp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFggjjz1HjUAw9bfOmwuTGhgSLCJEihMyleYm69cPKYMksJyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhMwAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY19jYTM2N2QxZmEwNDQ5NTEzMmFiMjczNWUxMTQ0NjMyYmhpbWFnZV9pZHgkZG9jX2NhMzY3ZDFmYTA0NDk1MTMyYWIyNzM1ZTExNDQ2MzJiaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMwWgAAAZ1qdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFpY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEAzMzliZmJhYTNhMjE4OGVjOTAzMmQ3YjNhMDRlMzg4YTZkMTY2OGZhYjk4OTZmMzYyYjUxOTAwNzc3ODZhN2JkaWRjOmZvcm1hdGlpbWFnZS9wbmdkZGF0YaVjdXJseJJodHRwczovL2FzeW5jLXByZXBhcmVkLWZpeHR1cmUucHVibGlzaGVyLmV4YW1wbGUuY29tL19lbmN5cGhlci9wcmVwYXJhdGlvbi9zb3VyY2UvMzM5YmZiYWEzYTIxODhlYzkwMzJkN2IzYTA0ZTM4OGE2ZDE2NjhmYWI5ODk2ZjM2MmI1MTkwMDc3Nzg2YTdiZGNhbGdmc2hhMjU2ZGhhc2hYIDOb+6o6IYjskDLXs6BOOIptFmj6uYlvNitRkAd3hqe9aWRjOmZvcm1hdGlpbWFnZS9wbmdkc2l6ZRjNAAAAj2p1bWIAAAAoanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5oYXNoLmRhdGEAAAAAX2Nib3KkamV4Y2x1c2lvbnOBomVzdGFydBjBZmxlbmd0aBmRlGNhbGdmc2hhMjU2ZGhhc2hYIDOb+6o6IYjskDLXs6BOOIptFmj6uYlvNitRkAd3hqe9Y3BhZEAAAAOIanVtYgAAACdqdW1kYzJjbAARABCAAACqADibcQNjMnBhLmNsYWltLnYyAAAAA1ljYm9ypmppbnN0YW5jZUlEeC11cm46dXVpZDo4Mjg0ZDY5MS0zY2ZjLTQ2N2YtYjdlOS1hYTk3MmNkMzJjNDF0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWV4G0VuY3lwaGVyIENETiBQcm92ZW5hbmNlLzEuMGd2ZXJzaW9uYzEuMGtzcGVjVmVyc2lvbmUyLjQuMGlzaWduYXR1cmV4TXNlbGYjanVtYmY9L2MycGEvdXJuOmMycGE6YTI1N2U4NWItMGVlOS00NGE4LThkMTEtMWI4MTRmNjNkZDE2L2MycGEuc2lnbmF0dXJlcmNyZWF0ZWRfYXNzZXJ0aW9uc4WjY3VybHgqc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5hY3Rpb25zLnYyZGhhc2hYIFgu0zlmAOM9AuMViaw9cvxmFc8Zk7v0O6eShCxAmFAXY2FsZ2ZzaGEyNTajY3VybHguc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvb3JnLmVuY3lwaGVyLnN0YXR1c2RoYXNoWCA4FE9UyyFdrzOpOKe9kPMZbSU+HOyWe5qXwYSVAq98nmNhbGdmc2hhMjU2o2N1cmx4MnNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlZGhhc2hYIJ0i6EARTP17OzcLYTjSyPfkDv+gA5dbdQnTUtquzkiZY2FsZ2ZzaGEyNTajY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzZGhhc2hYII489R41AMPW3zpsLkxoYEiwiRIoTMpXmJuvXDymDJLCY2FsZ2ZzaGEyNTajY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggQH5v7Kj4OmsaTLTPAb1YkvXs/mE0HT+TmpOoIV2kS79jYWxnZnNoYTI1NmNhbGdmc2hhMjU2aGRjOnRpdGxleEAzMzliZmJhYTNhMjE4OGVjOTAzMmQ3YjNhMDRlMzg4YTZkMTY2OGZhYjk4OTZmMzYyYjUxOTAwNzc3ODZhN2JkAACIjmp1bWIAAAAoanVtZGMyY3MAEQAQgAAAqgA4m3EDYzJwYS5zaWduYXR1cmUAAACIXmNib3LShFkH5aIBOCIYIYJZA+0wggPpMIIDbqADAgECAhQZfxi/dxd+bNlwYPcHxsoEJs9VajAKBggqhkjOPQQDAzCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEmMCQGA1UEAwwdRW5jeXBoZXIgQzJQQSBJc3N1aW5nIENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMjcxMTAzMDE0MTI5WjA/MQswCQYDVQQGEwJVUzEdMBsGA1UECgwURW5jeXBoZXIgQ29ycG9yYXRpb24xETAPBgNVBAMMCEVuY3lwaGVyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEO9nv54F/IOvYLjzmB5v3zhIY16xA2/7KY7eLf9+Um402shxJCZYObjhWjqZSRoIYYonOTjZl3Fv5nRyYmOkrpU2p30kQKh7lX10a/wrF9AjLH6LEl8BXgj8ocR5b2kIEo4IB1jCCAdIwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwIAYDVR0lBBkwFwYKKwYBBAGD6F4CAQYJKoZIhvcvAQEFMB0GA1UdDgQWBBQKuK/x4cC7/LOL0+nZrCatoD0aCjAfBgNVHSMEGDAWgBRlDB/oY/6Ws3C27VKnp+CheGDcSTA6BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC9pc3N1aW5nLWNhLmNybDB1BggrBgEFBQcBAQRpMGcwJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDA8BggrBgEFBQcwAoYwaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2lzc3VpbmctY2EuY3J0ME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczAZBgkrBgEEAYPoXgMEDAYKKwYBBAGD6F4DCjAzBgkrBgEEAYPoXgQEJgwkMDE5ZDIyNDEtZWI5Ny03NzI4LTllYzAtY2RhYWZiYTMwMGMyMAoGCCqGSM49BAMDA2kAMGYCMQD173r3fPhSgsvYV70EWRxuCWH4J8tE2OW6WAT9U/ItWvGBv8Vq20SCSEytYjJ9CaICMQD4m0wdj6vApL8/2jYxtV9Ifm29dxOteSk+qlodNVbuD8srY9/50y5FtWWKLvTnj2FZA+swggPnMIIDbqADAgECAhRPfUWzcQjoUDqL984Qz4bxxRtLMDAKBggqhkjOPQQDAzCBjjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEjMCEGA1UEAwwaRW5jeXBoZXIgQzJQQSBSb290IENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMzExMTAzMDE0MTI5WjCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEmMCQGA1UEAwwdRW5jeXBoZXIgQzJQQSBJc3N1aW5nIENBIDIwMjYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATnoZiNkz4Sbwsx6m0K3v0hFXsJMpYLvXMb0HYeVAvCbpQ0OBKPMGiRK9LqXtyydG+LEHyG3ydbnV0q4BSg+LxHPB5QwI+1PbwrtM1145MNf+R7DZjw9H0atzV8tzrMArqjggGGMIIBgjASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFGUMH+hj/pazcLbtUqen4KF4YNxJMB8GA1UdIwQYMBaAFOhuV2crBcyMC1h4cXy1kXTfuWoeMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3Jvb3QtY2EuY3JsMHIGCCsGAQUFBwEBBGYwZDAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDkGCCsGAQUFBzAChi1odHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvcm9vdC1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMAoGCCqGSM49BAMDA2cAMGQCMFcisvj1+nq8miEYn3hIuVYGzoIVIxJ+QAJ9htsB1augX/5YTt7WH8mN1+cYXFt2/AIwLluLOFCJtpZY6f03knMSgUQoro+FExPHBkCTbdkta8xZp7Tj/lKOzjBip9Q3bpyQomdzaWdUc3QyoWl0c3RUb2tlbnOBoWN2YWxZBqMwggafBgkqhkiG9w0BBwKgggaQMIIGjAIBAzEPMA0GCWCGSAFlAwQCAgUAMIGMBgsqhkiG9w0BCRABBKB9BHsweQIBAQYKKwYBBAGD6F4FATAxMA0GCWCGSAFlAwQCAQUABCBMQNbhTloqq5TUibfEyuF3M9GlrBBh4nwylL2RlrsPmAIULrBGUbkPR7tTB0lLOKkUuWqTPu4YDzIwMjYxMTAyMDE0MTMwWjADAgEBAgkAwVlkxFE8odagggPsMIID6DCCA26gAwIBAgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swCgYIKoZIzj0EAwMwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDIwMTQxMjlaMIGMMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSEwHwYDVQQDDBhFbmN5cGhlciBDMlBBIFRTQSBTaWduZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARr9gYQm+do/W6EIeKN2Uf5q9n8BoHmRqwDR/aTjv7mnsjg5PYBpoehnpzUp6iFBWDe8KAyOTjUx97QD60CJu2FULupHqIFTd5Lr8TD2903S/vL3PdOLM4mqhB/kyoxo4ajggGEMIIBgDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQUMtB7qVGEl2DENzmiThKHM9aMEK8wHwYDVR0jBBgwFoAUGVYfWNddcK6pMhCmHPMntLdClEswTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMHkGCCsGAQUFBwEBBG0wazAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMEAGCCsGAQUFBzAChjRodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvdHNhLWlzc3VpbmctY2EuY3J0MD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3RzYS1pc3N1aW5nLWNhLmNybDAKBggqhkjOPQQDAwNoADBlAjAxltHmUNm63uJrlWppEXbzH8U63vLO/KuiKZh7OyZLWJ73ocBZLrd0IHSxCz3Sln0CMQCYi4S4zwOX0Fg9xQh6/DLMy0O6jInZxIin+OGCbDxdhteAor5E8LFQj/VQwNqkGz0xggH1MIIB8QIBATCBrjCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2AhQoeKpdmqtzWZUbLWXhGy79mL0vizANBglghkgBZQMEAgIFAKCBtjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHgYJKoZIhvcNAQkFMREYDzIwMjYxMTAyMDE0MTMwWjA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCA8afx2houUS/YnXhA2Im3weOs9XV+BouMu8iSEfEywUjA/BgkqhkiG9w0BCQQxMgQwB5ka/j7OoWw0ZJhKA8C8HgMgWiSd3U4e+E+oAfvczidR6l3/b+0ympI5l2ep5XZFMAoGCCqGSM49BAMDBGcwZQIwTddm+/Yb9yoj4YDdiQJcuAIPJa/lfxiYBsQ2b2ktRUmNXwxWVVNs0gHSHfdVeQf4AjEAmh/GgAFxFVvmnIfURwU94qd4tuucbtU44fLtjxUCG0coFZvmPQRIQ11DMbF+AkO+GkNQQWRZeUEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2WGBTXvJWPx1Hok+smhORnTpm/jNT1ZWXWESi0Gd1NmZWEfMDWNi61OGXm3OfAdEqJ93+TTpfP2T/WM+MSdcimW50/gtVh/wdMZgqNseQ9Lc1J7RbjOdW1gVH6Y7DQnK1SosDfNl/AAAAAElFTkSuQmCC", + "planned": { + "digest": "339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd", + "geometry": { + "binding_recipe": { + "algorithm": "sha256", + "exclusions": [ + { + "length": 37268, + "offset": 193 + } + ], + "type": "data_hash" + }, + "carrier_output_span": [ + 193, + 37268 + ], + "carrier_recipe": { + "type": "standard" + }, + "carrier_span": [ + 0, + 37268 + ], + "disposition": "embedded", + "edits": [ + { + "length": 37268, + "offset": 193, + "op": "insert_carrier" + } + ], + "expected_final_size": 37473, + "planner_options": { + "tiff_placement": "main_ifd_when_single" + } + }, + "idempotency_key": "async-prepared-fixture:png:0a5f9c7ad0884a139e2a1cbcd5c514d6", + "plan_id": "de0832be-b673-439e-b7c5-bd0c92ffb46e", + "release": { + "abi": 1, + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "release": 18, + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f", + "signature": "33Dab6p1n5yJpIbXypes+yr6xXalcFonsh5h9YaFzMo18zFMHlNDXzriZNOe5LHZQ03P7Ki0T4ZXT+QNS6okCw==" + }, + "request": { + "carrier_requirement": "embedded_required", + "contract_version": 2, + "expected_policy_digest": "171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff", + "page_url": "https://async-prepared-fixture.publisher.example.com/article", + "probe": { + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f" + }, + "protocol": "encypher-prepared-signing/2", + "source": { + "byte_length": 205, + "mime": "image/png", + "origin_host": "async-prepared-fixture.publisher.example.com", + "reference": "https://async-prepared-fixture.publisher.example.com/_encypher/preparation/source/339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd", + "reference_kind": "publisher_snapshot", + "sha256": "339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd" + }, + "structure": [ + { + "b64": "iVBORw0KGgoAAAANSUhEUg==", + "offset": 0 + }, + { + "b64": "27ZsSwAAAJRJREFU", + "offset": 29 + }, + { + "b64": "6qgrjgAAAABJRU5ErkJggg==", + "offset": 189 + } + ] + } + }, + "source_b64": "iVBORw0KGgoAAAANSUhEUgAAADAAAAAgCAIAAADbtmxLAAAAlElEQVR4nO3XywqAIBCFYS8dWsz7v29bJS+Bo3MSYRbRYvz4KSLvnEMQnrni7WMgmp1BwgaiK7Qn6LxlPwOBDcRVCFQgEBbSBQ19N5Bcb1tIJ489KNeIPeg9liCUbp5CzTxmoJrGshARqJFHCzT8v5EYVhfK8pSOXgpCBWEB6p8i80H55vazPLPQ2EIlUG/Jxzwx+AdPEifC6qgrjgAAAABJRU5ErkJggg==" + }, + "protocol": "encypher-preparation/1", + "retirement_epoch": 0, + "variant": "" +} diff --git a/crates/provenance/encypher/src/fixtures/prepared/png/finalize.json b/crates/provenance/encypher/src/fixtures/prepared/png/finalize.json new file mode 100644 index 0000000000..ad9763b25c --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/png/finalize.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"de0832be-b673-439e-b7c5-bd0c92ffb46e","evidence_id":"38ae37ba-00f6-4dc5-94fe-d8c89716e19a","instance_id":"urn:uuid:8284d691-3cfc-467f-b7e9-aa972cd32c41","document_id":"doc_ca367d1fa04495132ab2735e1144632b","asset_id":"doc_ca367d1fa04495132ab2735e1144632b","mime":"image/png","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd","binding":{"type":"data_hash","digest":"339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd"},"manifest_store_b64":"AACRiGp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFianVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTphMjU3ZTg1Yi0wZWU5LTQ0YTgtOGQxMS0xYjgxNGY2M2RkMTYAAAAE/Wp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFggjjz1HjUAw9bfOmwuTGhgSLCJEihMyleYm69cPKYMksJyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhMwAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY19jYTM2N2QxZmEwNDQ5NTEzMmFiMjczNWUxMTQ0NjMyYmhpbWFnZV9pZHgkZG9jX2NhMzY3ZDFmYTA0NDk1MTMyYWIyNzM1ZTExNDQ2MzJiaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMwWgAAAZ1qdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFpY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEAzMzliZmJhYTNhMjE4OGVjOTAzMmQ3YjNhMDRlMzg4YTZkMTY2OGZhYjk4OTZmMzYyYjUxOTAwNzc3ODZhN2JkaWRjOmZvcm1hdGlpbWFnZS9wbmdkZGF0YaVjdXJseJJodHRwczovL2FzeW5jLXByZXBhcmVkLWZpeHR1cmUucHVibGlzaGVyLmV4YW1wbGUuY29tL19lbmN5cGhlci9wcmVwYXJhdGlvbi9zb3VyY2UvMzM5YmZiYWEzYTIxODhlYzkwMzJkN2IzYTA0ZTM4OGE2ZDE2NjhmYWI5ODk2ZjM2MmI1MTkwMDc3Nzg2YTdiZGNhbGdmc2hhMjU2ZGhhc2hYIDOb+6o6IYjskDLXs6BOOIptFmj6uYlvNitRkAd3hqe9aWRjOmZvcm1hdGlpbWFnZS9wbmdkc2l6ZRjNAAAAj2p1bWIAAAAoanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5oYXNoLmRhdGEAAAAAX2Nib3KkamV4Y2x1c2lvbnOBomVzdGFydBjBZmxlbmd0aBmRlGNhbGdmc2hhMjU2ZGhhc2hYIDOb+6o6IYjskDLXs6BOOIptFmj6uYlvNitRkAd3hqe9Y3BhZEAAAAOIanVtYgAAACdqdW1kYzJjbAARABCAAACqADibcQNjMnBhLmNsYWltLnYyAAAAA1ljYm9ypmppbnN0YW5jZUlEeC11cm46dXVpZDo4Mjg0ZDY5MS0zY2ZjLTQ2N2YtYjdlOS1hYTk3MmNkMzJjNDF0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWV4G0VuY3lwaGVyIENETiBQcm92ZW5hbmNlLzEuMGd2ZXJzaW9uYzEuMGtzcGVjVmVyc2lvbmUyLjQuMGlzaWduYXR1cmV4TXNlbGYjanVtYmY9L2MycGEvdXJuOmMycGE6YTI1N2U4NWItMGVlOS00NGE4LThkMTEtMWI4MTRmNjNkZDE2L2MycGEuc2lnbmF0dXJlcmNyZWF0ZWRfYXNzZXJ0aW9uc4WjY3VybHgqc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5hY3Rpb25zLnYyZGhhc2hYIFgu0zlmAOM9AuMViaw9cvxmFc8Zk7v0O6eShCxAmFAXY2FsZ2ZzaGEyNTajY3VybHguc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvb3JnLmVuY3lwaGVyLnN0YXR1c2RoYXNoWCA4FE9UyyFdrzOpOKe9kPMZbSU+HOyWe5qXwYSVAq98nmNhbGdmc2hhMjU2o2N1cmx4MnNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlZGhhc2hYIJ0i6EARTP17OzcLYTjSyPfkDv+gA5dbdQnTUtquzkiZY2FsZ2ZzaGEyNTajY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzZGhhc2hYII489R41AMPW3zpsLkxoYEiwiRIoTMpXmJuvXDymDJLCY2FsZ2ZzaGEyNTajY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggQH5v7Kj4OmsaTLTPAb1YkvXs/mE0HT+TmpOoIV2kS79jYWxnZnNoYTI1NmNhbGdmc2hhMjU2aGRjOnRpdGxleEAzMzliZmJhYTNhMjE4OGVjOTAzMmQ3YjNhMDRlMzg4YTZkMTY2OGZhYjk4OTZmMzYyYjUxOTAwNzc3ODZhN2JkAACIjmp1bWIAAAAoanVtZGMyY3MAEQAQgAAAqgA4m3EDYzJwYS5zaWduYXR1cmUAAACIXmNib3LShFkH5aIBOCIYIYJZA+0wggPpMIIDbqADAgECAhQZfxi/dxd+bNlwYPcHxsoEJs9VajAKBggqhkjOPQQDAzCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEmMCQGA1UEAwwdRW5jeXBoZXIgQzJQQSBJc3N1aW5nIENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMjcxMTAzMDE0MTI5WjA/MQswCQYDVQQGEwJVUzEdMBsGA1UECgwURW5jeXBoZXIgQ29ycG9yYXRpb24xETAPBgNVBAMMCEVuY3lwaGVyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEO9nv54F/IOvYLjzmB5v3zhIY16xA2/7KY7eLf9+Um402shxJCZYObjhWjqZSRoIYYonOTjZl3Fv5nRyYmOkrpU2p30kQKh7lX10a/wrF9AjLH6LEl8BXgj8ocR5b2kIEo4IB1jCCAdIwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwIAYDVR0lBBkwFwYKKwYBBAGD6F4CAQYJKoZIhvcvAQEFMB0GA1UdDgQWBBQKuK/x4cC7/LOL0+nZrCatoD0aCjAfBgNVHSMEGDAWgBRlDB/oY/6Ws3C27VKnp+CheGDcSTA6BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC9pc3N1aW5nLWNhLmNybDB1BggrBgEFBQcBAQRpMGcwJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDA8BggrBgEFBQcwAoYwaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2lzc3VpbmctY2EuY3J0ME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczAZBgkrBgEEAYPoXgMEDAYKKwYBBAGD6F4DCjAzBgkrBgEEAYPoXgQEJgwkMDE5ZDIyNDEtZWI5Ny03NzI4LTllYzAtY2RhYWZiYTMwMGMyMAoGCCqGSM49BAMDA2kAMGYCMQD173r3fPhSgsvYV70EWRxuCWH4J8tE2OW6WAT9U/ItWvGBv8Vq20SCSEytYjJ9CaICMQD4m0wdj6vApL8/2jYxtV9Ifm29dxOteSk+qlodNVbuD8srY9/50y5FtWWKLvTnj2FZA+swggPnMIIDbqADAgECAhRPfUWzcQjoUDqL984Qz4bxxRtLMDAKBggqhkjOPQQDAzCBjjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEjMCEGA1UEAwwaRW5jeXBoZXIgQzJQQSBSb290IENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMzExMTAzMDE0MTI5WjCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEmMCQGA1UEAwwdRW5jeXBoZXIgQzJQQSBJc3N1aW5nIENBIDIwMjYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATnoZiNkz4Sbwsx6m0K3v0hFXsJMpYLvXMb0HYeVAvCbpQ0OBKPMGiRK9LqXtyydG+LEHyG3ydbnV0q4BSg+LxHPB5QwI+1PbwrtM1145MNf+R7DZjw9H0atzV8tzrMArqjggGGMIIBgjASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFGUMH+hj/pazcLbtUqen4KF4YNxJMB8GA1UdIwQYMBaAFOhuV2crBcyMC1h4cXy1kXTfuWoeMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3Jvb3QtY2EuY3JsMHIGCCsGAQUFBwEBBGYwZDAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDkGCCsGAQUFBzAChi1odHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvcm9vdC1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMAoGCCqGSM49BAMDA2cAMGQCMFcisvj1+nq8miEYn3hIuVYGzoIVIxJ+QAJ9htsB1augX/5YTt7WH8mN1+cYXFt2/AIwLluLOFCJtpZY6f03knMSgUQoro+FExPHBkCTbdkta8xZp7Tj/lKOzjBip9Q3bpyQomdzaWdUc3QyoWl0c3RUb2tlbnOBoWN2YWxZBqMwggafBgkqhkiG9w0BBwKgggaQMIIGjAIBAzEPMA0GCWCGSAFlAwQCAgUAMIGMBgsqhkiG9w0BCRABBKB9BHsweQIBAQYKKwYBBAGD6F4FATAxMA0GCWCGSAFlAwQCAQUABCBMQNbhTloqq5TUibfEyuF3M9GlrBBh4nwylL2RlrsPmAIULrBGUbkPR7tTB0lLOKkUuWqTPu4YDzIwMjYxMTAyMDE0MTMwWjADAgEBAgkAwVlkxFE8odagggPsMIID6DCCA26gAwIBAgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swCgYIKoZIzj0EAwMwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDIwMTQxMjlaMIGMMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSEwHwYDVQQDDBhFbmN5cGhlciBDMlBBIFRTQSBTaWduZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARr9gYQm+do/W6EIeKN2Uf5q9n8BoHmRqwDR/aTjv7mnsjg5PYBpoehnpzUp6iFBWDe8KAyOTjUx97QD60CJu2FULupHqIFTd5Lr8TD2903S/vL3PdOLM4mqhB/kyoxo4ajggGEMIIBgDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQUMtB7qVGEl2DENzmiThKHM9aMEK8wHwYDVR0jBBgwFoAUGVYfWNddcK6pMhCmHPMntLdClEswTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMHkGCCsGAQUFBwEBBG0wazAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMEAGCCsGAQUFBzAChjRodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvdHNhLWlzc3VpbmctY2EuY3J0MD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3RzYS1pc3N1aW5nLWNhLmNybDAKBggqhkjOPQQDAwNoADBlAjAxltHmUNm63uJrlWppEXbzH8U63vLO/KuiKZh7OyZLWJ73ocBZLrd0IHSxCz3Sln0CMQCYi4S4zwOX0Fg9xQh6/DLMy0O6jInZxIin+OGCbDxdhteAor5E8LFQj/VQwNqkGz0xggH1MIIB8QIBATCBrjCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2AhQoeKpdmqtzWZUbLWXhGy79mL0vizANBglghkgBZQMEAgIFAKCBtjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHgYJKoZIhvcNAQkFMREYDzIwMjYxMTAyMDE0MTMwWjA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCA8afx2houUS/YnXhA2Im3weOs9XV+BouMu8iSEfEywUjA/BgkqhkiG9w0BCQQxMgQwB5ka/j7OoWw0ZJhKA8C8HgMgWiSd3U4e+E+oAfvczidR6l3/b+0ympI5l2ep5XZFMAoGCCqGSM49BAMDBGcwZQIwTddm+/Yb9yoj4YDdiQJcuAIPJa/lfxiYBsQ2b2ktRUmNXwxWVVNs0gHSHfdVeQf4AjEAmh/GgAFxFVvmnIfURwU94qd4tuucbtU44fLtjxUCG0coFZvmPQRIQ11DMbF+AkO+GkNQQWRZeUEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2WGBTXvJWPx1Hok+smhORnTpm/jNT1ZWXWESi0Gd1NmZWEfMDWNi61OGXm3OfAdEqJ93+TTpfP2T/WM+MSdcimW50/gtVh/wdMZgqNseQ9Lc1J7RbjOdW1gVH6Y7DQnK1Sos=","manifest_sha256":"7ec447d2036bf438267003e6128c877f3aeac8347658c8d0b47d25a7cf6a577f","carrier_sha256":"da22252001c353dad718ccbeb4ad4080364b37c52e2a0407e9833d15489c30ce","expected_final_size":37473,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_ca367d1fa04495132ab2735e1144632b","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/png/output.png b/crates/provenance/encypher/src/fixtures/prepared/png/output.png new file mode 100644 index 0000000000..c0a057e59c Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/png/output.png differ diff --git a/crates/provenance/encypher/src/fixtures/prepared/png/prepare.json b/crates/provenance/encypher/src/fixtures/prepared/png/prepare.json new file mode 100644 index 0000000000..e712b8a7dc --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/png/prepare.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"de0832be-b673-439e-b7c5-bd0c92ffb46e","instance_id":"urn:uuid:8284d691-3cfc-467f-b7e9-aa972cd32c41","document_id":"doc_ca367d1fa04495132ab2735e1144632b","expires_at":"2026-11-02T02:41:30.153426Z","mime":"image/png","expected_manifest_size":37256,"effective_action":"c2pa.opened","geometry":{"disposition":"embedded","edits":[{"op":"insert_carrier","offset":193,"length":37268}],"carrier_span":[0,37268],"carrier_output_span":[193,37268],"expected_final_size":37473,"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[{"offset":193,"length":37268}]},"carrier_recipe":{"type":"standard"},"planner_options":{"tiff_placement":"main_ifd_when_single"}},"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/png/recover.json b/crates/provenance/encypher/src/fixtures/prepared/png/recover.json new file mode 100644 index 0000000000..f73ded5103 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/png/recover.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff","prepare":{"protocol":"encypher-prepared-signing/2","plan_id":"de0832be-b673-439e-b7c5-bd0c92ffb46e","instance_id":"urn:uuid:8284d691-3cfc-467f-b7e9-aa972cd32c41","document_id":"doc_ca367d1fa04495132ab2735e1144632b","expires_at":"2026-11-02T02:41:30.153426Z","mime":"image/png","expected_manifest_size":37256,"effective_action":"c2pa.opened","geometry":{"edits":[{"op":"insert_carrier","length":37268,"offset":193}],"disposition":"embedded","carrier_span":[0,37268],"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[{"length":37268,"offset":193}]},"carrier_recipe":{"type":"standard"},"planner_options":{"tiff_placement":"main_ifd_when_single"},"carrier_output_span":[193,37268],"expected_final_size":37473},"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"},"finalize":{"protocol":"encypher-prepared-signing/2","plan_id":"de0832be-b673-439e-b7c5-bd0c92ffb46e","evidence_id":"38ae37ba-00f6-4dc5-94fe-d8c89716e19a","instance_id":"urn:uuid:8284d691-3cfc-467f-b7e9-aa972cd32c41","document_id":"doc_ca367d1fa04495132ab2735e1144632b","asset_id":"doc_ca367d1fa04495132ab2735e1144632b","mime":"image/png","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd","binding":{"type":"data_hash","digest":"339bfbaa3a2188ec9032d7b3a04e388a6d1668fab9896f362b5190077786a7bd"},"manifest_store_b64":"AACRiGp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFianVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTphMjU3ZTg1Yi0wZWU5LTQ0YTgtOGQxMS0xYjgxNGY2M2RkMTYAAAAE/Wp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFggjjz1HjUAw9bfOmwuTGhgSLCJEihMyleYm69cPKYMksJyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhMwAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY19jYTM2N2QxZmEwNDQ5NTEzMmFiMjczNWUxMTQ0NjMyYmhpbWFnZV9pZHgkZG9jX2NhMzY3ZDFmYTA0NDk1MTMyYWIyNzM1ZTExNDQ2MzJiaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMwWgAAAZ1qdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFpY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEAzMzliZmJhYTNhMjE4OGVjOTAzMmQ3YjNhMDRlMzg4YTZkMTY2OGZhYjk4OTZmMzYyYjUxOTAwNzc3ODZhN2JkaWRjOmZvcm1hdGlpbWFnZS9wbmdkZGF0YaVjdXJseJJodHRwczovL2FzeW5jLXByZXBhcmVkLWZpeHR1cmUucHVibGlzaGVyLmV4YW1wbGUuY29tL19lbmN5cGhlci9wcmVwYXJhdGlvbi9zb3VyY2UvMzM5YmZiYWEzYTIxODhlYzkwMzJkN2IzYTA0ZTM4OGE2ZDE2NjhmYWI5ODk2ZjM2MmI1MTkwMDc3Nzg2YTdiZGNhbGdmc2hhMjU2ZGhhc2hYIDOb+6o6IYjskDLXs6BOOIptFmj6uYlvNitRkAd3hqe9aWRjOmZvcm1hdGlpbWFnZS9wbmdkc2l6ZRjNAAAAj2p1bWIAAAAoanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5oYXNoLmRhdGEAAAAAX2Nib3KkamV4Y2x1c2lvbnOBomVzdGFydBjBZmxlbmd0aBmRlGNhbGdmc2hhMjU2ZGhhc2hYIDOb+6o6IYjskDLXs6BOOIptFmj6uYlvNitRkAd3hqe9Y3BhZEAAAAOIanVtYgAAACdqdW1kYzJjbAARABCAAACqADibcQNjMnBhLmNsYWltLnYyAAAAA1ljYm9ypmppbnN0YW5jZUlEeC11cm46dXVpZDo4Mjg0ZDY5MS0zY2ZjLTQ2N2YtYjdlOS1hYTk3MmNkMzJjNDF0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWV4G0VuY3lwaGVyIENETiBQcm92ZW5hbmNlLzEuMGd2ZXJzaW9uYzEuMGtzcGVjVmVyc2lvbmUyLjQuMGlzaWduYXR1cmV4TXNlbGYjanVtYmY9L2MycGEvdXJuOmMycGE6YTI1N2U4NWItMGVlOS00NGE4LThkMTEtMWI4MTRmNjNkZDE2L2MycGEuc2lnbmF0dXJlcmNyZWF0ZWRfYXNzZXJ0aW9uc4WjY3VybHgqc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5hY3Rpb25zLnYyZGhhc2hYIFgu0zlmAOM9AuMViaw9cvxmFc8Zk7v0O6eShCxAmFAXY2FsZ2ZzaGEyNTajY3VybHguc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvb3JnLmVuY3lwaGVyLnN0YXR1c2RoYXNoWCA4FE9UyyFdrzOpOKe9kPMZbSU+HOyWe5qXwYSVAq98nmNhbGdmc2hhMjU2o2N1cmx4MnNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlZGhhc2hYIJ0i6EARTP17OzcLYTjSyPfkDv+gA5dbdQnTUtquzkiZY2FsZ2ZzaGEyNTajY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzZGhhc2hYII489R41AMPW3zpsLkxoYEiwiRIoTMpXmJuvXDymDJLCY2FsZ2ZzaGEyNTajY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggQH5v7Kj4OmsaTLTPAb1YkvXs/mE0HT+TmpOoIV2kS79jYWxnZnNoYTI1NmNhbGdmc2hhMjU2aGRjOnRpdGxleEAzMzliZmJhYTNhMjE4OGVjOTAzMmQ3YjNhMDRlMzg4YTZkMTY2OGZhYjk4OTZmMzYyYjUxOTAwNzc3ODZhN2JkAACIjmp1bWIAAAAoanVtZGMyY3MAEQAQgAAAqgA4m3EDYzJwYS5zaWduYXR1cmUAAACIXmNib3LShFkH5aIBOCIYIYJZA+0wggPpMIIDbqADAgECAhQZfxi/dxd+bNlwYPcHxsoEJs9VajAKBggqhkjOPQQDAzCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEmMCQGA1UEAwwdRW5jeXBoZXIgQzJQQSBJc3N1aW5nIENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMjcxMTAzMDE0MTI5WjA/MQswCQYDVQQGEwJVUzEdMBsGA1UECgwURW5jeXBoZXIgQ29ycG9yYXRpb24xETAPBgNVBAMMCEVuY3lwaGVyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEO9nv54F/IOvYLjzmB5v3zhIY16xA2/7KY7eLf9+Um402shxJCZYObjhWjqZSRoIYYonOTjZl3Fv5nRyYmOkrpU2p30kQKh7lX10a/wrF9AjLH6LEl8BXgj8ocR5b2kIEo4IB1jCCAdIwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwIAYDVR0lBBkwFwYKKwYBBAGD6F4CAQYJKoZIhvcvAQEFMB0GA1UdDgQWBBQKuK/x4cC7/LOL0+nZrCatoD0aCjAfBgNVHSMEGDAWgBRlDB/oY/6Ws3C27VKnp+CheGDcSTA6BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC9pc3N1aW5nLWNhLmNybDB1BggrBgEFBQcBAQRpMGcwJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDA8BggrBgEFBQcwAoYwaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2lzc3VpbmctY2EuY3J0ME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczAZBgkrBgEEAYPoXgMEDAYKKwYBBAGD6F4DCjAzBgkrBgEEAYPoXgQEJgwkMDE5ZDIyNDEtZWI5Ny03NzI4LTllYzAtY2RhYWZiYTMwMGMyMAoGCCqGSM49BAMDA2kAMGYCMQD173r3fPhSgsvYV70EWRxuCWH4J8tE2OW6WAT9U/ItWvGBv8Vq20SCSEytYjJ9CaICMQD4m0wdj6vApL8/2jYxtV9Ifm29dxOteSk+qlodNVbuD8srY9/50y5FtWWKLvTnj2FZA+swggPnMIIDbqADAgECAhRPfUWzcQjoUDqL984Qz4bxxRtLMDAKBggqhkjOPQQDAzCBjjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEjMCEGA1UEAwwaRW5jeXBoZXIgQzJQQSBSb290IENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMzExMTAzMDE0MTI5WjCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEmMCQGA1UEAwwdRW5jeXBoZXIgQzJQQSBJc3N1aW5nIENBIDIwMjYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATnoZiNkz4Sbwsx6m0K3v0hFXsJMpYLvXMb0HYeVAvCbpQ0OBKPMGiRK9LqXtyydG+LEHyG3ydbnV0q4BSg+LxHPB5QwI+1PbwrtM1145MNf+R7DZjw9H0atzV8tzrMArqjggGGMIIBgjASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFGUMH+hj/pazcLbtUqen4KF4YNxJMB8GA1UdIwQYMBaAFOhuV2crBcyMC1h4cXy1kXTfuWoeMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3Jvb3QtY2EuY3JsMHIGCCsGAQUFBwEBBGYwZDAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDkGCCsGAQUFBzAChi1odHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvcm9vdC1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMAoGCCqGSM49BAMDA2cAMGQCMFcisvj1+nq8miEYn3hIuVYGzoIVIxJ+QAJ9htsB1augX/5YTt7WH8mN1+cYXFt2/AIwLluLOFCJtpZY6f03knMSgUQoro+FExPHBkCTbdkta8xZp7Tj/lKOzjBip9Q3bpyQomdzaWdUc3QyoWl0c3RUb2tlbnOBoWN2YWxZBqMwggafBgkqhkiG9w0BBwKgggaQMIIGjAIBAzEPMA0GCWCGSAFlAwQCAgUAMIGMBgsqhkiG9w0BCRABBKB9BHsweQIBAQYKKwYBBAGD6F4FATAxMA0GCWCGSAFlAwQCAQUABCBMQNbhTloqq5TUibfEyuF3M9GlrBBh4nwylL2RlrsPmAIULrBGUbkPR7tTB0lLOKkUuWqTPu4YDzIwMjYxMTAyMDE0MTMwWjADAgEBAgkAwVlkxFE8odagggPsMIID6DCCA26gAwIBAgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swCgYIKoZIzj0EAwMwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDIwMTQxMjlaMIGMMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSEwHwYDVQQDDBhFbmN5cGhlciBDMlBBIFRTQSBTaWduZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARr9gYQm+do/W6EIeKN2Uf5q9n8BoHmRqwDR/aTjv7mnsjg5PYBpoehnpzUp6iFBWDe8KAyOTjUx97QD60CJu2FULupHqIFTd5Lr8TD2903S/vL3PdOLM4mqhB/kyoxo4ajggGEMIIBgDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQUMtB7qVGEl2DENzmiThKHM9aMEK8wHwYDVR0jBBgwFoAUGVYfWNddcK6pMhCmHPMntLdClEswTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMHkGCCsGAQUFBwEBBG0wazAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMEAGCCsGAQUFBzAChjRodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvdHNhLWlzc3VpbmctY2EuY3J0MD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL3RzYS1pc3N1aW5nLWNhLmNybDAKBggqhkjOPQQDAwNoADBlAjAxltHmUNm63uJrlWppEXbzH8U63vLO/KuiKZh7OyZLWJ73ocBZLrd0IHSxCz3Sln0CMQCYi4S4zwOX0Fg9xQh6/DLMy0O6jInZxIin+OGCbDxdhteAor5E8LFQj/VQwNqkGz0xggH1MIIB8QIBATCBrjCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2AhQoeKpdmqtzWZUbLWXhGy79mL0vizANBglghkgBZQMEAgIFAKCBtjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHgYJKoZIhvcNAQkFMREYDzIwMjYxMTAyMDE0MTMwWjA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCA8afx2houUS/YnXhA2Im3weOs9XV+BouMu8iSEfEywUjA/BgkqhkiG9w0BCQQxMgQwB5ka/j7OoWw0ZJhKA8C8HgMgWiSd3U4e+E+oAfvczidR6l3/b+0ympI5l2ep5XZFMAoGCCqGSM49BAMDBGcwZQIwTddm+/Yb9yoj4YDdiQJcuAIPJa/lfxiYBsQ2b2ktRUmNXwxWVVNs0gHSHfdVeQf4AjEAmh/GgAFxFVvmnIfURwU94qd4tuucbtU44fLtjxUCG0coFZvmPQRIQ11DMbF+AkO+GkNQQWRZeUEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2WGBTXvJWPx1Hok+smhORnTpm/jNT1ZWXWESi0Gd1NmZWEfMDWNi61OGXm3OfAdEqJ93+TTpfP2T/WM+MSdcimW50/gtVh/wdMZgqNseQ9Lc1J7RbjOdW1gVH6Y7DQnK1Sos=","manifest_sha256":"7ec447d2036bf438267003e6128c877f3aeac8347658c8d0b47d25a7cf6a577f","carrier_sha256":"da22252001c353dad718ccbeb4ad4080364b37c52e2a0407e9833d15489c30ce","expected_final_size":37473,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_ca367d1fa04495132ab2735e1144632b","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"}} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/png/source.png b/crates/provenance/encypher/src/fixtures/prepared/png/source.png new file mode 100644 index 0000000000..0b577c44c1 Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/png/source.png differ diff --git a/crates/provenance/encypher/src/fixtures/prepared/probe.wasm b/crates/provenance/encypher/src/fixtures/prepared/probe.wasm new file mode 100644 index 0000000000..eee02f5bdd Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/probe.wasm differ diff --git a/crates/provenance/encypher/src/fixtures/prepared/release-key.hex b/crates/provenance/encypher/src/fixtures/prepared/release-key.hex new file mode 100644 index 0000000000..fb7b7903fe --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/release-key.hex @@ -0,0 +1 @@ +9ae0bf4aea5140008d2f48e6c08691360a7fe23e0d36f24522c770b7646b75d9 diff --git a/crates/provenance/encypher/src/fixtures/prepared/svg/envelope.json b/crates/provenance/encypher/src/fixtures/prepared/svg/envelope.json new file mode 100644 index 0000000000..4f84afcb7a --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/svg/envelope.json @@ -0,0 +1,84 @@ +{ + "deployment_id": "async-prepared-api-fixture", + "identity": "90e6a23f07c50dbcc73acd642b278c9e0ff15caff8b4096b7a9ff63b36939222", + "item": { + "kind": "embedded", + "output_b64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDMyIj48bWV0YWRhdGE+PGMycGE6bWFuaWZlc3QgeG1sbnM6YzJwYT0iaHR0cDovL2MycGEub3JnL21hbmlmZXN0Ij5BQUNSa0dwMWJXSUFBQUFlYW5WdFpHTXljR0VBRVFBUWdBQUFxZ0E0bTNFRFl6SndZUUFBQUpGcWFuVnRZZ0FBQUVkcWRXMWtZekp0WVFBUkFCQ0FBQUNxQURpYmNRTjFjbTQ2WXpKd1lUb3pPV1kyT1RaalpDMWxaRFF3TFRSak9XRXRZakE1TUMxalpERmhOR1EwWlRVeU9HRUFBQUFGQldwMWJXSUFBQUFwYW5WdFpHTXlZWE1BRVFBUWdBQUFxZ0E0bTNFRFl6SndZUzVoYzNObGNuUnBiMjV6QUFBQUFPcHFkVzFpQUFBQUtXcDFiV1JqWW05eUFCRUFFSUFBQUtvQU9KdHhBMk15Y0dFdVlXTjBhVzl1Y3k1Mk1nQUFBQUM1WTJKdmNxSm5ZV04wYVc5dWM0R2labUZqZEdsdmJtdGpNbkJoTG05d1pXNWxaR3B3WVhKaGJXVjBaWEp6b1d0cGJtZHlaV1JwWlc1MGM0R2pZM1Z5YkhndGMyVnNaaU5xZFcxaVpqMWpNbkJoTG1GemMyVnlkR2x2Ym5Ndll6SndZUzVwYm1keVpXUnBaVzUwTG5ZelkyRnNaMlp6YUdFeU5UWmthR0Z6YUZnZzBtWklZbmhsSjNieEcwT0taeGpFVHRFZktFU1VMaFFJS080b3J2S05RWGx5WVd4c1FXTjBhVzl1YzBsdVkyeDFaR1ZrOVFBQUFMZHFkVzFpQUFBQUxXcDFiV1JqWW05eUFCRUFFSUFBQUtvQU9KdHhBMjl5Wnk1bGJtTjVjR2hsY2k1emRHRjBkWE1BQUFBQWdtTmliM0tpZEhOMFlYUjFjMHhwYzNSRGNtVmtaVzUwYVdGc2VGQm9kSFJ3Y3pvdkwzWmxjbWxtZVM1bGJtTjVjR2hsY2k1amIyMHZjM1JoZEhWekwzWXhMMnhwYzNSekwySmpabVV3TURFMExUazFaV0V0TkdJMk1TMWlaR05oTFRnek1ERTVOV0UwTkdOa05XOXpkR0YwZFhOTWFYTjBTVzVrWlhoaE53QUFBUDlxZFcxaUFBQUFNV3AxYldSalltOXlBQkVBRUlBQUFLb0FPSnR4QTJOdmJTNWxibU41Y0dobGNpNXdjbTkyWlc1aGJtTmxBQUFBQU1aalltOXlwVzl2Y21kaGJtbDZZWFJwYjI1ZmFXUjBiM0puWDJOa2JsODBPRE5rWVdZMU1HUTJORFZwYzJsbmJtVnlYMmxrYldWdVkzbHdhR1Z5TG1aeVpXVnJaRzlqZFcxbGJuUmZhV1I0SkdSdlkxOHhOVEppT1RabVpqaGpabVF5WkdZNU1tVTNZbVV4TnpVek5qQXhPREZrWW1ocGJXRm5aVjlwWkhna1pHOWpYekUxTW1JNU5tWm1PR05tWkRKa1pqa3laVGRpWlRFM05UTTJNREU0TVdSaWFYTnBaMjVsWkY5aGRIUXlNREkyTFRFeExUQXlWREF4T2pReE9qTXdXZ0FBQWFWcWRXMWlBQUFBTEdwMWJXUmpZbTl5QUJFQUVJQUFBS29BT0p0eEEyTXljR0V1YVc1bmNtVmthV1Z1ZEM1Mk13QUFBQUZ4WTJKdmNxUnNjbVZzWVhScGIyNXphR2x3YUhCaGNtVnVkRTltYUdSak9uUnBkR3hsZUVBd1pqWTVNekE0TnpRMVpqSTRNelUwWXpRMU9EbGpOVGhsTnpkbE5UZ3laRFJtWTJaa05EVXpPRGxtWlRFME56VmlObVpsTTJZNVlqZGhOVE13WXpCaGFXUmpPbVp2Y20xaGRHMXBiV0ZuWlM5emRtY3JlRzFzWkdSaGRHR2xZM1Z5YkhpU2FIUjBjSE02THk5aGMzbHVZeTF3Y21Wd1lYSmxaQzFtYVhoMGRYSmxMbkIxWW14cGMyaGxjaTVsZUdGdGNHeGxMbU52YlM5ZlpXNWplWEJvWlhJdmNISmxjR0Z5WVhScGIyNHZjMjkxY21ObEx6Qm1Oamt6TURnM05EVm1Namd6TlRSak5EVTRPV00xT0dVM04yVTFPREprTkdaalptUTBOVE00T1dabE1UUTNOV0kyWm1VelpqbGlOMkUxTXpCak1HRmpZV3huWm5Ob1lUSTFObVJvWVhOb1dDQVBhVENIUmZLRFZNUlluRmpuZmxndFQ4L1VVNG4rRkhXMi9qK2JlbE1NQ21sa1l6cG1iM0p0WVhSdGFXMWhaMlV2YzNabkszaHRiR1J6YVhwbEdMUUFBQUNQYW5WdFlnQUFBQ2hxZFcxa1kySnZjZ0FSQUJDQUFBQ3FBRGliY1FOak1uQmhMbWhoYzJndVpHRjBZUUFBQUFCZlkySnZjcVJxWlhoamJIVnphVzl1YzRHaVpYTjBZWEowR0ZObWJHVnVaM1JvR2NKeVkyRnNaMlp6YUdFeU5UWmthR0Z6YUZnZ0Qya3doMFh5ZzFURVdKeFk1MzVZTFUvUDFGT0ovaFIxdHY0L20zcFREQXBqY0dGa1FBQUFBNGhxZFcxaUFBQUFKMnAxYldSak1tTnNBQkVBRUlBQUFLb0FPSnR4QTJNeWNHRXVZMnhoYVcwdWRqSUFBQUFEV1dOaWIzS21hbWx1YzNSaGJtTmxTVVI0TFhWeWJqcDFkV2xrT2pVeVpXWmxNVEEwTFRsbU5qTXROR0UwTnkwNE5qRXdMVEJtWXpVM09UWmlZamN5TW5SamJHRnBiVjluWlc1bGNtRjBiM0pmYVc1bWI2TmtibUZ0WlhnYlJXNWplWEJvWlhJZ1EwUk9JRkJ5YjNabGJtRnVZMlV2TVM0d1ozWmxjbk5wYjI1ak1TNHdhM053WldOV1pYSnphVzl1WlRJdU5DNHdhWE5wWjI1aGRIVnlaWGhOYzJWc1ppTnFkVzFpWmowdll6SndZUzkxY200Nll6SndZVG96T1dZMk9UWmpaQzFsWkRRd0xUUmpPV0V0WWpBNU1DMWpaREZoTkdRMFpUVXlPR0V2WXpKd1lTNXphV2R1WVhSMWNtVnlZM0psWVhSbFpGOWhjM05sY25ScGIyNXpoYU5qZFhKc2VDcHpaV3htSTJwMWJXSm1QV015Y0dFdVlYTnpaWEowYVc5dWN5OWpNbkJoTG1GamRHbHZibk11ZGpKa2FHRnphRmdnaHBLQTB3VzFqM0l0ejZDOVA1VkhPNVFBYzBKS2t0dTU2RWZJMzNhSTYzQmpZV3huWm5Ob1lUSTFOcU5qZFhKc2VDNXpaV3htSTJwMWJXSm1QV015Y0dFdVlYTnpaWEowYVc5dWN5OXZjbWN1Wlc1amVYQm9aWEl1YzNSaGRIVnpaR2hoYzJoWUlHck5IRVAyeVdRYlptSFRieCtvaWlldzNWY2lTSkdKQVBXRFNzVkRlTWljWTJGc1oyWnphR0V5TlRhalkzVnliSGd5YzJWc1ppTnFkVzFpWmoxak1uQmhMbUZ6YzJWeWRHbHZibk12WTI5dExtVnVZM2x3YUdWeUxuQnliM1psYm1GdVkyVmthR0Z6YUZnZzV3WTk0Rk5zYWlLbnFWZ05iRTVhcmxJZ0wxQi9aQlJMN1FDUHlScTc2MEpqWVd4blpuTm9ZVEkxTnFOamRYSnNlQzF6Wld4bUkycDFiV0ptUFdNeWNHRXVZWE56WlhKMGFXOXVjeTlqTW5CaExtbHVaM0psWkdsbGJuUXVkak5rYUdGemFGZ2cwbVpJWW5obEozYnhHME9LWnhqRVR0RWZLRVNVTGhRSUtPNG9ydktOUVhsallXeG5abk5vWVRJMU5xTmpkWEpzZUNselpXeG1JMnAxYldKbVBXTXljR0V1WVhOelpYSjBhVzl1Y3k5ak1uQmhMbWhoYzJndVpHRjBZV1JvWVhOb1dDQWNmdmdIc3RLWkQ4UGVhbEN0V2lOSHF4Zi9MYWJCYTZNclpzaEk3dFV4L21OaGJHZG1jMmhoTWpVMlkyRnNaMlp6YUdFeU5UWm9aR002ZEdsMGJHVjRRREJtTmprek1EZzNORFZtTWpnek5UUmpORFU0T1dNMU9HVTNOMlUxT0RKa05HWmpabVEwTlRNNE9XWmxNVFEzTldJMlptVXpaamxpTjJFMU16QmpNR0VBQUlpT2FuVnRZZ0FBQUNocWRXMWtZekpqY3dBUkFCQ0FBQUNxQURpYmNRTmpNbkJoTG5OcFoyNWhkSFZ5WlFBQUFJaGVZMkp2Y3RLRVdRZmxvZ0U0SWhnaGdsa0Q3VENDQStrd2dnTnVvQU1DQVFJQ0ZCbC9HTDkzRjM1czJYQmc5d2ZHeWdRbXoxVnFNQW9HQ0NxR1NNNDlCQU1ETUlHUk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVYTUJVR0ExVUVDZ3dPUlc1amVYQm9aWElnUTI5eWNDNHhGREFTQmdOVkJBc01DME5CSUVScGRtbHphVzl1TVNZd0pBWURWUVFEREIxRmJtTjVjR2hsY2lCRE1sQkJJRWx6YzNWcGJtY2dRMEVnTWpBeU5qQWVGdzB5TmpFeE1ESXdNVFF4TWpsYUZ3MHlOekV4TURNd01UUXhNamxhTUQ4eEN6QUpCZ05WQkFZVEFsVlRNUjB3R3dZRFZRUUtEQlJGYm1ONWNHaGxjaUJEYjNKd2IzSmhkR2x2YmpFUk1BOEdBMVVFQXd3SVJXNWplWEJvWlhJd2RqQVFCZ2NxaGtqT1BRSUJCZ1VyZ1FRQUlnTmlBQVE3MmUvbmdYOGc2OWd1UE9ZSG0vZk9FaGpYckVEYi9zcGp0NHQvMzVTYmpUYXlIRWtKbGc1dU9GYU9wbEpHZ2hoaWljNU9ObVhjVy9tZEhKaVk2U3VsVGFuZlNSQXFIdVZmWFJyL0NzWDBDTXNmb3NTWHdGZUNQeWh4SGx2YVFnU2pnZ0hXTUlJQjBqQU1CZ05WSFJNQkFmOEVBakFBTUE0R0ExVWREd0VCL3dRRUF3SUd3REFnQmdOVkhTVUVHVEFYQmdvckJnRUVBWVBvWGdJQkJna3Foa2lHOXk4QkFRVXdIUVlEVlIwT0JCWUVGQXE0ci9IaHdMdjhzNHZUNmRtc0pxMmdQUm9LTUI4R0ExVWRJd1FZTUJhQUZHVU1IK2hqL3BhemNMYnRVcWVuNEtGNFlOeEpNRG9HQTFVZEh3UXpNREV3TDZBdG9DdUdLV2gwZEhBNkx5OWpZUzVsYm1ONWNHaGxjaTVqYjIwdlkzSnNMMmx6YzNWcGJtY3RZMkV1WTNKc01IVUdDQ3NHQVFVRkJ3RUJCR2t3WnpBbkJnZ3JCZ0VGQlFjd0FZWWJhSFIwY0RvdkwyTmhMbVZ1WTNsd2FHVnlMbU52YlM5dlkzTndNRHdHQ0NzR0FRVUZCekFDaGpCb2RIUndPaTh2WTJFdVpXNWplWEJvWlhJdVkyOXRMM0psY0c5emFYUnZjbmt2YVhOemRXbHVaeTFqWVM1amNuUXdUUVlEVlIwZ0JFWXdSREJDQmdvckJnRUVBWVBvWGdFQk1EUXdNZ1lJS3dZQkJRVUhBZ0VXSm1oMGRIQnpPaTh2WTJFdVpXNWplWEJvWlhJdVkyOXRMM0psY0c5emFYUnZjbmt2WTNCek1Ca0dDU3NHQVFRQmcraGVBd1FNQmdvckJnRUVBWVBvWGdNS01ETUdDU3NHQVFRQmcraGVCQVFtRENRd01UbGtNakkwTVMxbFlqazNMVGMzTWpndE9XVmpNQzFqWkdGaFptSmhNekF3WXpJd0NnWUlLb1pJemowRUF3TURhUUF3WmdJeEFQWHZldmQ4K0ZLQ3k5aFh2UVJaSEc0SllmZ255MFRZNWJwWUJQMVQ4aTFhOFlHL3hXcmJSSUpJVEsxaU1uMEpvZ0l4QVBpYlRCMlBxOENrdnovYU5qRzFYMGgrYmIxM0U2MTVLVDZxV2gwMVZ1NFB5eXRqMy9uVExrVzFaWW91OU9lUFlWa0Q2ekNDQStjd2dnTnVvQU1DQVFJQ0ZFOTlSYk54Q09oUU9vdjN6aERQaHZIRkcwc3dNQW9HQ0NxR1NNNDlCQU1ETUlHT01Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVYTUJVR0ExVUVDZ3dPUlc1amVYQm9aWElnUTI5eWNDNHhGREFTQmdOVkJBc01DME5CSUVScGRtbHphVzl1TVNNd0lRWURWUVFEREJwRmJtTjVjR2hsY2lCRE1sQkJJRkp2YjNRZ1EwRWdNakF5TmpBZUZ3MHlOakV4TURJd01UUXhNamxhRncwek1URXhNRE13TVRReE1qbGFNSUdSTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RVhNQlVHQTFVRUNnd09SVzVqZVhCb1pYSWdRMjl5Y0M0eEZEQVNCZ05WQkFzTUMwTkJJRVJwZG1semFXOXVNU1l3SkFZRFZRUUREQjFGYm1ONWNHaGxjaUJETWxCQklFbHpjM1ZwYm1jZ1EwRWdNakF5TmpCMk1CQUdCeXFHU000OUFnRUdCU3VCQkFBaUEySUFCT2VobUkyVFBoSnZDekhxYlFyZS9TRVZld2t5bGd1OWN4dlFkaDVVQzhKdWxEUTRFbzh3YUpFcjB1cGUzTEowYjRzUWZJYmZKMXVkWFNyZ0ZLRDR2RWM4SGxEQWo3VTl2Q3UwelhYamt3MS81SHNObVBEMGZScTNOWHkzT3N3Q3VxT0NBWVl3Z2dHQ01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRQXdEZ1lEVlIwUEFRSC9CQVFEQWdFR01DQUdBMVVkSlFRWk1CY0dDaXNHQVFRQmcraGVBZ0VHQ1NxR1NJYjNMd0VCQlRBZEJnTlZIUTRFRmdRVVpRd2Y2R1ArbHJOd3R1MVNwNmZnb1hoZzNFa3dId1lEVlIwakJCZ3dGb0FVNkc1WFp5c0Z6SXdMV0hoeGZMV1JkTis1YWg0d053WURWUjBmQkRBd0xqQXNvQ3FnS0lZbWFIUjBjRG92TDJOaExtVnVZM2x3YUdWeUxtTnZiUzlqY213dmNtOXZkQzFqWVM1amNtd3djZ1lJS3dZQkJRVUhBUUVFWmpCa01DY0dDQ3NHQVFVRkJ6QUJoaHRvZEhSd09pOHZZMkV1Wlc1amVYQm9aWEl1WTI5dEwyOWpjM0F3T1FZSUt3WUJCUVVITUFLR0xXaDBkSEE2THk5allTNWxibU41Y0dobGNpNWpiMjB2Y21Wd2IzTnBkRzl5ZVM5eWIyOTBMV05oTG1OeWREQk5CZ05WSFNBRVJqQkVNRUlHQ2lzR0FRUUJnK2hlQVFFd05EQXlCZ2dyQmdFRkJRY0NBUlltYUhSMGNITTZMeTlqWVM1bGJtTjVjR2hsY2k1amIyMHZjbVZ3YjNOcGRHOXllUzlqY0hNd0NnWUlLb1pJemowRUF3TURad0F3WkFJd1Z5S3krUFg2ZXJ5YUlSaWZlRWk1VmdiT2doVWpFbjVBQW4yRzJ3SFZxNkJmL2xoTzN0WWZ5WTNYNXhoY1czYjhBakF1VzRzNFVJbTJsbGpwL1RlU2N4S0JSQ2l1ajRVVEU4Y0dRSk50MlMxcnpGbW50T1ArVW83T01HS24xRGR1bkpDaVozTnBaMVJ6ZERLaGFYUnpkRlJ2YTJWdWM0R2hZM1poYkZrR296Q0NCcDhHQ1NxR1NJYjNEUUVIQXFDQ0JwQXdnZ2FNQWdFRE1ROHdEUVlKWUlaSUFXVURCQUlDQlFBd2dZd0dDeXFHU0liM0RRRUpFQUVFb0gwRWV6QjVBZ0VCQmdvckJnRUVBWVBvWGdVQk1ERXdEUVlKWUlaSUFXVURCQUlCQlFBRUlGN0ZSZlFwb0ZLb1FmQ3djMEQ4Z09ZQmtEZElRMUgxSk5LckxlSmkycWlQQWhRWkVtT3lGQ204dDI4d2xiQldSb0lpY1ZwT1pSZ1BNakF5TmpFeE1ESXdNVFF4TXpGYU1BTUNBUUVDQ1FDU2xhY0JwcGtPSEtDQ0Erd3dnZ1BvTUlJRGJxQURBZ0VDQWhRb2VLcGRtcXR6V1pVYkxXWGhHeTc5bUwwdml6QUtCZ2dxaGtqT1BRUURBekNCbFRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhGekFWQmdOVkJBb01Ea1Z1WTNsd2FHVnlJRU52Y25BdU1SUXdFZ1lEVlFRTERBdERRU0JFYVhacGMybHZiakVxTUNnR0ExVUVBd3doUlc1amVYQm9aWElnUXpKUVFTQlVVMEVnU1hOemRXbHVaeUJEUVNBeU1ESTJNQjRYRFRJMk1URXdNakF4TkRFeU9Wb1hEVEkzTVRFd01qQXhOREV5T1Zvd2dZd3hDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SWXdGQVlEVlFRSERBMVRZVzRnUm5KaGJtTnBjMk52TVJjd0ZRWURWUVFLREE1RmJtTjVjR2hsY2lCRGIzSndMakVVTUJJR0ExVUVDd3dMUTBFZ1JHbDJhWE5wYjI0eElUQWZCZ05WQkFNTUdFVnVZM2x3YUdWeUlFTXlVRUVnVkZOQklGTnBaMjVsY2pCMk1CQUdCeXFHU000OUFnRUdCU3VCQkFBaUEySUFCR3YyQmhDYjUyajlib1FoNG8zWlIvbXIyZndHZ2VaR3JBTkg5cE9PL3VhZXlPRGs5Z0dtaDZHZW5OU25xSVVGWU43d29ESTVPTlRIM3RBUHJRSW03WVZRdTZrZW9nVk4za3V2eE1QYjNUZEwrOHZjOTA0c3ppYXFFSCtUS2pHamhxT0NBWVF3Z2dHQU1Bd0dBMVVkRXdFQi93UUNNQUF3RGdZRFZSMFBBUUgvQkFRREFnYkFNQllHQTFVZEpRRUIvd1FNTUFvR0NDc0dBUVVGQndNSU1CMEdBMVVkRGdRV0JCUXkwSHVwVVlTWFlNUTNPYUpPRW9jejFvd1FyekFmQmdOVkhTTUVHREFXZ0JRWlZoOVkxMTF3cnFreUVLWWM4eWUwdDBLVVN6Qk5CZ05WSFNBRVJqQkVNRUlHQ2lzR0FRUUJnK2hlQVFFd05EQXlCZ2dyQmdFRkJRY0NBUlltYUhSMGNITTZMeTlqWVM1bGJtTjVjR2hsY2k1amIyMHZjbVZ3YjNOcGRHOXllUzlqY0hNd2VRWUlLd1lCQlFVSEFRRUViVEJyTUNjR0NDc0dBUVVGQnpBQmhodG9kSFJ3T2k4dlkyRXVaVzVqZVhCb1pYSXVZMjl0TDI5amMzQXdRQVlJS3dZQkJRVUhNQUtHTkdoMGRIQTZMeTlqWVM1bGJtTjVjR2hsY2k1amIyMHZjbVZ3YjNOcGRHOXllUzkwYzJFdGFYTnpkV2x1WnkxallTNWpjblF3UGdZRFZSMGZCRGN3TlRBem9ER2dMNFl0YUhSMGNEb3ZMMk5oTG1WdVkzbHdhR1Z5TG1OdmJTOWpjbXd2ZEhOaExXbHpjM1ZwYm1jdFkyRXVZM0pzTUFvR0NDcUdTTTQ5QkFNREEyZ0FNR1VDTURHVzBlWlEyYnJlNG11VmFta1Jkdk1meFRyZThzNzhxNklwbUhzN0prdFludmVod0ZrdXQzUWdkTEVMUGRLV2ZRSXhBSmlMaExqUEE1ZlFXRDNGQ0hyOE1zekxRN3FNaWRuRWlLZjQ0WUpzUEYyRzE0Q2l2a1R3c1ZDUDlWREEycVFiUFRHQ0FmVXdnZ0h4QWdFQk1JR3VNSUdWTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RVhNQlVHQTFVRUNnd09SVzVqZVhCb1pYSWdRMjl5Y0M0eEZEQVNCZ05WQkFzTUMwTkJJRVJwZG1semFXOXVNU293S0FZRFZRUUREQ0ZGYm1ONWNHaGxjaUJETWxCQklGUlRRU0JKYzNOMWFXNW5JRU5CSURJd01qWUNGQ2g0cWwyYXEzTlpsUnN0WmVFYkx2Mll2UytMTUEwR0NXQ0dTQUZsQXdRQ0FnVUFvSUcyTUJvR0NTcUdTSWIzRFFFSkF6RU5CZ3NxaGtpRzl3MEJDUkFCQkRBZUJna3Foa2lHOXcwQkNRVXhFUmdQTWpBeU5qRXhNREl3TVRReE16RmFNRGNHQ3lxR1NJYjNEUUVKRUFJdk1TZ3dKakFrTUNJRUlEeHAvSGFHaTVSTDlpZGVFRFlpYmZCNDZ6MWRYNEdpNHk3eUpJUjhUTEJTTUQ4R0NTcUdTSWIzRFFFSkJERXlCRENyeWdpMUozL3lMSWp6cXVWZk82SFd1bkMxMkIrdUlqNHMwdVNwOThPZUlUK2hlbEhGQnBXMkoyRkNPK1VlMDBnd0NnWUlLb1pJemowRUF3TUVaekJsQWpFQXVsRGhGbHVkcGJyS1BlTmpOUitPQVI5ZGtLaktEbU8yc3N4Y1gvQlNEd05nc0FTcmU4UVV3MkZadEZuS2tFTHBBakFaY0N5dHJ5OHpzM3pkdE1sVUNDVUsrb3UyODVrLzFmR252eUliVzZrVGdkVVJCNXJENXJyZW5jTFl6RTNyS3lBYVExQkJaRmw1UVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQVBaWVlMUVYzYTJXSkpUcEZrZVRmRkVLNVJjQ1VtdWZnby9RUnNIZFM1NnpQclpINDJnUEl0NmVZOHFuU29XVkhXakRRZmFsRjBta1B5RFppbWJCZm85T1VZWTk1RXBWN2pBY0YvNHZPTW9nbGhKV1ZmZlhpQVExU0k2S0FpRTZZanlxd0E9PTwvYzJwYTptYW5pZmVzdD48L21ldGFkYXRhPjxyZWN0IHdpZHRoPSI0OCIgaGVpZ2h0PSIzMiIgZmlsbD0iIzE0NWFhMCIvPjxjaXJjbGUgY3g9IjI0IiBjeT0iMTYiIHI9IjgiIGZpbGw9IiNlOGFjMzgiLz48L3N2Zz4=", + "planned": { + "digest": "0f69308745f28354c4589c58e77e582d4fcfd45389fe1475b6fe3f9b7a530c0a", + "geometry": { + "binding_recipe": { + "algorithm": "sha256", + "exclusions": [ + { + "length": 49778, + "offset": 83 + } + ], + "type": "data_hash" + }, + "carrier_output_span": [ + 83, + 49778 + ], + "carrier_recipe": { + "type": "standard" + }, + "carrier_span": [ + 0, + 49778 + ], + "disposition": "embedded", + "edits": [ + { + "length": 49778, + "offset": 83, + "op": "insert_carrier" + } + ], + "expected_final_size": 49958, + "planner_options": { + "tiff_placement": "main_ifd_when_single" + } + }, + "idempotency_key": "async-prepared-fixture:svg:dc922f35245c4cab9f29b3fd8278f4ab", + "plan_id": "18ec3a3b-1a2c-4c84-a43d-1e3da58a30ad", + "release": { + "abi": 1, + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "release": 18, + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f", + "signature": "33Dab6p1n5yJpIbXypes+yr6xXalcFonsh5h9YaFzMo18zFMHlNDXzriZNOe5LHZQ03P7Ki0T4ZXT+QNS6okCw==" + }, + "request": { + "carrier_requirement": "embedded_required", + "contract_version": 2, + "expected_policy_digest": "171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff", + "page_url": "https://async-prepared-fixture.publisher.example.com/article", + "probe": { + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f" + }, + "protocol": "encypher-prepared-signing/2", + "source": { + "byte_length": 180, + "mime": "image/svg+xml", + "origin_host": "async-prepared-fixture.publisher.example.com", + "reference": "https://async-prepared-fixture.publisher.example.com/_encypher/preparation/source/0f69308745f28354c4589c58e77e582d4fcfd45389fe1475b6fe3f9b7a530c0a", + "reference_kind": "publisher_snapshot", + "sha256": "0f69308745f28354c4589c58e77e582d4fcfd45389fe1475b6fe3f9b7a530c0a" + }, + "structure": [ + { + "b64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDMyIj4=", + "offset": 0 + } + ] + } + }, + "source_b64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDMyIj48cmVjdCB3aWR0aD0iNDgiIGhlaWdodD0iMzIiIGZpbGw9IiMxNDVhYTAiLz48Y2lyY2xlIGN4PSIyNCIgY3k9IjE2IiByPSI4IiBmaWxsPSIjZThhYzM4Ii8+PC9zdmc+" + }, + "protocol": "encypher-preparation/1", + "retirement_epoch": 0, + "variant": "" +} diff --git a/crates/provenance/encypher/src/fixtures/prepared/svg/finalize.json b/crates/provenance/encypher/src/fixtures/prepared/svg/finalize.json new file mode 100644 index 0000000000..e06f56d180 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/svg/finalize.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"18ec3a3b-1a2c-4c84-a43d-1e3da58a30ad","evidence_id":"ef0ab30a-2a24-49aa-b022-43b071b75084","instance_id":"urn:uuid:52efe104-9f63-4a47-8610-0fc5796bb722","document_id":"doc_152b96ff8cfd2df92e7be175360181db","asset_id":"doc_152b96ff8cfd2df92e7be175360181db","mime":"image/svg+xml","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"0f69308745f28354c4589c58e77e582d4fcfd45389fe1475b6fe3f9b7a530c0a","binding":{"type":"data_hash","digest":"0f69308745f28354c4589c58e77e582d4fcfd45389fe1475b6fe3f9b7a530c0a"},"manifest_store_b64":"AACRkGp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFqanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTozOWY2OTZjZC1lZDQwLTRjOWEtYjA5MC1jZDFhNGQ0ZTUyOGEAAAAFBWp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFgg0mZIYnhlJ3bxG0OKZxjETtEfKESULhQIKO4orvKNQXlyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhNwAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY18xNTJiOTZmZjhjZmQyZGY5MmU3YmUxNzUzNjAxODFkYmhpbWFnZV9pZHgkZG9jXzE1MmI5NmZmOGNmZDJkZjkyZTdiZTE3NTM2MDE4MWRiaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMwWgAAAaVqdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFxY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEAwZjY5MzA4NzQ1ZjI4MzU0YzQ1ODljNThlNzdlNTgyZDRmY2ZkNDUzODlmZTE0NzViNmZlM2Y5YjdhNTMwYzBhaWRjOmZvcm1hdG1pbWFnZS9zdmcreG1sZGRhdGGlY3VybHiSaHR0cHM6Ly9hc3luYy1wcmVwYXJlZC1maXh0dXJlLnB1Ymxpc2hlci5leGFtcGxlLmNvbS9fZW5jeXBoZXIvcHJlcGFyYXRpb24vc291cmNlLzBmNjkzMDg3NDVmMjgzNTRjNDU4OWM1OGU3N2U1ODJkNGZjZmQ0NTM4OWZlMTQ3NWI2ZmUzZjliN2E1MzBjMGFjYWxnZnNoYTI1NmRoYXNoWCAPaTCHRfKDVMRYnFjnflgtT8/UU4n+FHW2/j+belMMCmlkYzpmb3JtYXRtaW1hZ2Uvc3ZnK3htbGRzaXplGLQAAACPanVtYgAAAChqdW1kY2JvcgARABCAAACqADibcQNjMnBhLmhhc2guZGF0YQAAAABfY2JvcqRqZXhjbHVzaW9uc4GiZXN0YXJ0GFNmbGVuZ3RoGcJyY2FsZ2ZzaGEyNTZkaGFzaFggD2kwh0Xyg1TEWJxY535YLU/P1FOJ/hR1tv4/m3pTDApjcGFkQAAAA4hqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADWWNib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOjUyZWZlMTA0LTlmNjMtNGE0Ny04NjEwLTBmYzU3OTZiYjcyMnRjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYTozOWY2OTZjZC1lZDQwLTRjOWEtYjA5MC1jZDFhNGQ0ZTUyOGEvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgghpKA0wW1j3Itz6C9P5VHO5QAc0JKktu56EfI33aI63BjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIGrNHEP2yWQbZmHTbx+oiiew3VciSJGJAPWDSsVDeMicY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFgg5wY94FNsaiKnqVgNbE5arlIgL1B/ZBRL7QCPyRq760JjYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFgg0mZIYnhlJ3bxG0OKZxjETtEfKESULhQIKO4orvKNQXljYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCAcfvgHstKZD8PealCtWiNHqxf/LabBa6MrZshI7tUx/mNhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGV4QDBmNjkzMDg3NDVmMjgzNTRjNDU4OWM1OGU3N2U1ODJkNGZjZmQ0NTM4OWZlMTQ3NWI2ZmUzZjliN2E1MzBjMGEAAIiOanVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAAIheY2JvctKEWQflogE4IhghglkD7TCCA+kwggNuoAMCAQICFBl/GL93F35s2XBg9wfGygQmz1VqMAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDMwMTQxMjlaMD8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKDBRFbmN5cGhlciBDb3Jwb3JhdGlvbjERMA8GA1UEAwwIRW5jeXBoZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ72e/ngX8g69guPOYHm/fOEhjXrEDb/spjt4t/35SbjTayHEkJlg5uOFaOplJGghhiic5ONmXcW/mdHJiY6SulTanfSRAqHuVfXRr/CsX0CMsfosSXwFeCPyhxHlvaQgSjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFAq4r/HhwLv8s4vT6dmsJq2gPRoKMB8GA1UdIwQYMBaAFGUMH+hj/pazcLbtUqen4KF4YNxJMDoGA1UdHwQzMDEwL6AtoCuGKWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL2lzc3VpbmctY2EuY3JsMHUGCCsGAQUFBwEBBGkwZzAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDwGCCsGAQUFBzAChjBodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvaXNzdWluZy1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMBkGCSsGAQQBg+heAwQMBgorBgEEAYPoXgMKMDMGCSsGAQQBg+heBAQmDCQwMTlkMjI0MS1lYjk3LTc3MjgtOWVjMC1jZGFhZmJhMzAwYzIwCgYIKoZIzj0EAwMDaQAwZgIxAPXvevd8+FKCy9hXvQRZHG4JYfgny0TY5bpYBP1T8i1a8YG/xWrbRIJITK1iMn0JogIxAPibTB2Pq8Ckvz/aNjG1X0h+bb13E615KT6qWh01Vu4Pyytj3/nTLkW1ZYou9OePYVkD6zCCA+cwggNuoAMCAQICFE99RbNxCOhQOov3zhDPhvHFG0swMAoGCCqGSM49BAMDMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSMwIQYDVQQDDBpFbmN5cGhlciBDMlBBIFJvb3QgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0zMTExMDMwMTQxMjlaMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOehmI2TPhJvCzHqbQre/SEVewkylgu9cxvQdh5UC8JulDQ4Eo8waJEr0upe3LJ0b4sQfIbfJ1udXSrgFKD4vEc8HlDAj7U9vCu0zXXjkw1/5HsNmPD0fRq3NXy3OswCuqOCAYYwggGCMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwHwYDVR0jBBgwFoAU6G5XZysFzIwLWHhxfLWRdN+5ah4wNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvcm9vdC1jYS5jcmwwcgYIKwYBBQUHAQEEZjBkMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwOQYIKwYBBQUHMAKGLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9yb290LWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwCgYIKoZIzj0EAwMDZwAwZAIwVyKy+PX6eryaIRifeEi5VgbOghUjEn5AAn2G2wHVq6Bf/lhO3tYfyY3X5xhcW3b8AjAuW4s4UIm2lljp/TeScxKBRCiuj4UTE8cGQJNt2S1rzFmntOP+Uo7OMGKn1DdunJCiZ3NpZ1RzdDKhaXRzdFRva2Vuc4GhY3ZhbFkGozCCBp8GCSqGSIb3DQEHAqCCBpAwggaMAgEDMQ8wDQYJYIZIAWUDBAICBQAwgYwGCyqGSIb3DQEJEAEEoH0EezB5AgEBBgorBgEEAYPoXgUBMDEwDQYJYIZIAWUDBAIBBQAEIF7FRfQpoFKoQfCwc0D8gOYBkDdIQ1H1JNKrLeJi2qiPAhQZEmOyFCm8t28wlbBWRoIicVpOZRgPMjAyNjExMDIwMTQxMzFaMAMCAQECCQCSlacBppkOHKCCA+wwggPoMIIDbqADAgECAhQoeKpdmqtzWZUbLWXhGy79mL0vizAKBggqhkjOPQQDAzCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMjAxNDEyOVowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xITAfBgNVBAMMGEVuY3lwaGVyIEMyUEEgVFNBIFNpZ25lcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABGv2BhCb52j9boQh4o3ZR/mr2fwGgeZGrANH9pOO/uaeyODk9gGmh6GenNSnqIUFYN7woDI5ONTH3tAPrQIm7YVQu6keogVN3kuvxMPb3TdL+8vc904sziaqEH+TKjGjhqOCAYQwggGAMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMB0GA1UdDgQWBBQy0HupUYSXYMQ3OaJOEocz1owQrzAfBgNVHSMEGDAWgBQZVh9Y111wrqkyEKYc8ye0t0KUSzBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMweQYIKwYBBQUHAQEEbTBrMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS90c2EtaXNzdWluZy1jYS5jcnQwPgYDVR0fBDcwNTAzoDGgL4YtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvdHNhLWlzc3VpbmctY2EuY3JsMAoGCCqGSM49BAMDA2gAMGUCMDGW0eZQ2bre4muVamkRdvMfxTre8s78q6IpmHs7JktYnvehwFkut3QgdLELPdKWfQIxAJiLhLjPA5fQWD3FCHr8MszLQ7qMidnEiKf44YJsPF2G14CivkTwsVCP9VDA2qQbPTGCAfUwggHxAgEBMIGuMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSowKAYDVQQDDCFFbmN5cGhlciBDMlBBIFRTQSBJc3N1aW5nIENBIDIwMjYCFCh4ql2aq3NZlRstZeEbLv2YvS+LMA0GCWCGSAFlAwQCAgUAoIG2MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAeBgkqhkiG9w0BCQUxERgPMjAyNjExMDIwMTQxMzFaMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIDxp/HaGi5RL9ideEDYibfB46z1dX4Gi4y7yJIR8TLBSMD8GCSqGSIb3DQEJBDEyBDCrygi1J3/yLIjzquVfO6HWunC12B+uIj4s0uSp98OeIT+helHFBpW2J2FCO+Ue00gwCgYIKoZIzj0EAwMEZzBlAjEAulDhFludpbrKPeNjNR+OAR9dkKjKDmO2ssxcX/BSDwNgsASre8QUw2FZtFnKkELpAjAZcCytry8zs3zdtMlUCCUK+ou285k/1fGnvyIbW6kTgdURB5rD5rrencLYzE3rKyAaQ1BBZFl5QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYYLQV3a2WJJTpFkeTfFEK5RcCUmufgo/QRsHdS56zPrZH42gPIt6eY8qnSoWVHWjDQfalF0mkPyDZimbBfo9OUYY95EpV7jAcF/4vOMoglhJWVffXiAQ1SI6KAiE6YjyqwA==","manifest_sha256":"6115eb178d5eefaf985f93c858ab0cca0d9f2b39780206efca5bb39903d6b974","carrier_sha256":"2f203a04cff50e55f5f6a9ffcef6e4bb179abb7faf0a8aa1f43d3be59b3eeae6","expected_final_size":49958,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_152b96ff8cfd2df92e7be175360181db","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/svg/output.svg b/crates/provenance/encypher/src/fixtures/prepared/svg/output.svg new file mode 100644 index 0000000000..a936313918 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/svg/output.svg @@ -0,0 +1 @@ +AACRkGp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFqanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTozOWY2OTZjZC1lZDQwLTRjOWEtYjA5MC1jZDFhNGQ0ZTUyOGEAAAAFBWp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFgg0mZIYnhlJ3bxG0OKZxjETtEfKESULhQIKO4orvKNQXlyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhNwAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY18xNTJiOTZmZjhjZmQyZGY5MmU3YmUxNzUzNjAxODFkYmhpbWFnZV9pZHgkZG9jXzE1MmI5NmZmOGNmZDJkZjkyZTdiZTE3NTM2MDE4MWRiaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMwWgAAAaVqdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFxY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEAwZjY5MzA4NzQ1ZjI4MzU0YzQ1ODljNThlNzdlNTgyZDRmY2ZkNDUzODlmZTE0NzViNmZlM2Y5YjdhNTMwYzBhaWRjOmZvcm1hdG1pbWFnZS9zdmcreG1sZGRhdGGlY3VybHiSaHR0cHM6Ly9hc3luYy1wcmVwYXJlZC1maXh0dXJlLnB1Ymxpc2hlci5leGFtcGxlLmNvbS9fZW5jeXBoZXIvcHJlcGFyYXRpb24vc291cmNlLzBmNjkzMDg3NDVmMjgzNTRjNDU4OWM1OGU3N2U1ODJkNGZjZmQ0NTM4OWZlMTQ3NWI2ZmUzZjliN2E1MzBjMGFjYWxnZnNoYTI1NmRoYXNoWCAPaTCHRfKDVMRYnFjnflgtT8/UU4n+FHW2/j+belMMCmlkYzpmb3JtYXRtaW1hZ2Uvc3ZnK3htbGRzaXplGLQAAACPanVtYgAAAChqdW1kY2JvcgARABCAAACqADibcQNjMnBhLmhhc2guZGF0YQAAAABfY2JvcqRqZXhjbHVzaW9uc4GiZXN0YXJ0GFNmbGVuZ3RoGcJyY2FsZ2ZzaGEyNTZkaGFzaFggD2kwh0Xyg1TEWJxY535YLU/P1FOJ/hR1tv4/m3pTDApjcGFkQAAAA4hqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADWWNib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOjUyZWZlMTA0LTlmNjMtNGE0Ny04NjEwLTBmYzU3OTZiYjcyMnRjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYTozOWY2OTZjZC1lZDQwLTRjOWEtYjA5MC1jZDFhNGQ0ZTUyOGEvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgghpKA0wW1j3Itz6C9P5VHO5QAc0JKktu56EfI33aI63BjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIGrNHEP2yWQbZmHTbx+oiiew3VciSJGJAPWDSsVDeMicY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFgg5wY94FNsaiKnqVgNbE5arlIgL1B/ZBRL7QCPyRq760JjYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFgg0mZIYnhlJ3bxG0OKZxjETtEfKESULhQIKO4orvKNQXljYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCAcfvgHstKZD8PealCtWiNHqxf/LabBa6MrZshI7tUx/mNhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGV4QDBmNjkzMDg3NDVmMjgzNTRjNDU4OWM1OGU3N2U1ODJkNGZjZmQ0NTM4OWZlMTQ3NWI2ZmUzZjliN2E1MzBjMGEAAIiOanVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAAIheY2JvctKEWQflogE4IhghglkD7TCCA+kwggNuoAMCAQICFBl/GL93F35s2XBg9wfGygQmz1VqMAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDMwMTQxMjlaMD8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKDBRFbmN5cGhlciBDb3Jwb3JhdGlvbjERMA8GA1UEAwwIRW5jeXBoZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ72e/ngX8g69guPOYHm/fOEhjXrEDb/spjt4t/35SbjTayHEkJlg5uOFaOplJGghhiic5ONmXcW/mdHJiY6SulTanfSRAqHuVfXRr/CsX0CMsfosSXwFeCPyhxHlvaQgSjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFAq4r/HhwLv8s4vT6dmsJq2gPRoKMB8GA1UdIwQYMBaAFGUMH+hj/pazcLbtUqen4KF4YNxJMDoGA1UdHwQzMDEwL6AtoCuGKWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL2lzc3VpbmctY2EuY3JsMHUGCCsGAQUFBwEBBGkwZzAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDwGCCsGAQUFBzAChjBodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvaXNzdWluZy1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMBkGCSsGAQQBg+heAwQMBgorBgEEAYPoXgMKMDMGCSsGAQQBg+heBAQmDCQwMTlkMjI0MS1lYjk3LTc3MjgtOWVjMC1jZGFhZmJhMzAwYzIwCgYIKoZIzj0EAwMDaQAwZgIxAPXvevd8+FKCy9hXvQRZHG4JYfgny0TY5bpYBP1T8i1a8YG/xWrbRIJITK1iMn0JogIxAPibTB2Pq8Ckvz/aNjG1X0h+bb13E615KT6qWh01Vu4Pyytj3/nTLkW1ZYou9OePYVkD6zCCA+cwggNuoAMCAQICFE99RbNxCOhQOov3zhDPhvHFG0swMAoGCCqGSM49BAMDMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSMwIQYDVQQDDBpFbmN5cGhlciBDMlBBIFJvb3QgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0zMTExMDMwMTQxMjlaMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOehmI2TPhJvCzHqbQre/SEVewkylgu9cxvQdh5UC8JulDQ4Eo8waJEr0upe3LJ0b4sQfIbfJ1udXSrgFKD4vEc8HlDAj7U9vCu0zXXjkw1/5HsNmPD0fRq3NXy3OswCuqOCAYYwggGCMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwHwYDVR0jBBgwFoAU6G5XZysFzIwLWHhxfLWRdN+5ah4wNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvcm9vdC1jYS5jcmwwcgYIKwYBBQUHAQEEZjBkMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwOQYIKwYBBQUHMAKGLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9yb290LWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwCgYIKoZIzj0EAwMDZwAwZAIwVyKy+PX6eryaIRifeEi5VgbOghUjEn5AAn2G2wHVq6Bf/lhO3tYfyY3X5xhcW3b8AjAuW4s4UIm2lljp/TeScxKBRCiuj4UTE8cGQJNt2S1rzFmntOP+Uo7OMGKn1DdunJCiZ3NpZ1RzdDKhaXRzdFRva2Vuc4GhY3ZhbFkGozCCBp8GCSqGSIb3DQEHAqCCBpAwggaMAgEDMQ8wDQYJYIZIAWUDBAICBQAwgYwGCyqGSIb3DQEJEAEEoH0EezB5AgEBBgorBgEEAYPoXgUBMDEwDQYJYIZIAWUDBAIBBQAEIF7FRfQpoFKoQfCwc0D8gOYBkDdIQ1H1JNKrLeJi2qiPAhQZEmOyFCm8t28wlbBWRoIicVpOZRgPMjAyNjExMDIwMTQxMzFaMAMCAQECCQCSlacBppkOHKCCA+wwggPoMIIDbqADAgECAhQoeKpdmqtzWZUbLWXhGy79mL0vizAKBggqhkjOPQQDAzCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMjAxNDEyOVowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xITAfBgNVBAMMGEVuY3lwaGVyIEMyUEEgVFNBIFNpZ25lcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABGv2BhCb52j9boQh4o3ZR/mr2fwGgeZGrANH9pOO/uaeyODk9gGmh6GenNSnqIUFYN7woDI5ONTH3tAPrQIm7YVQu6keogVN3kuvxMPb3TdL+8vc904sziaqEH+TKjGjhqOCAYQwggGAMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMB0GA1UdDgQWBBQy0HupUYSXYMQ3OaJOEocz1owQrzAfBgNVHSMEGDAWgBQZVh9Y111wrqkyEKYc8ye0t0KUSzBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMweQYIKwYBBQUHAQEEbTBrMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS90c2EtaXNzdWluZy1jYS5jcnQwPgYDVR0fBDcwNTAzoDGgL4YtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvdHNhLWlzc3VpbmctY2EuY3JsMAoGCCqGSM49BAMDA2gAMGUCMDGW0eZQ2bre4muVamkRdvMfxTre8s78q6IpmHs7JktYnvehwFkut3QgdLELPdKWfQIxAJiLhLjPA5fQWD3FCHr8MszLQ7qMidnEiKf44YJsPF2G14CivkTwsVCP9VDA2qQbPTGCAfUwggHxAgEBMIGuMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSowKAYDVQQDDCFFbmN5cGhlciBDMlBBIFRTQSBJc3N1aW5nIENBIDIwMjYCFCh4ql2aq3NZlRstZeEbLv2YvS+LMA0GCWCGSAFlAwQCAgUAoIG2MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAeBgkqhkiG9w0BCQUxERgPMjAyNjExMDIwMTQxMzFaMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIDxp/HaGi5RL9ideEDYibfB46z1dX4Gi4y7yJIR8TLBSMD8GCSqGSIb3DQEJBDEyBDCrygi1J3/yLIjzquVfO6HWunC12B+uIj4s0uSp98OeIT+helHFBpW2J2FCO+Ue00gwCgYIKoZIzj0EAwMEZzBlAjEAulDhFludpbrKPeNjNR+OAR9dkKjKDmO2ssxcX/BSDwNgsASre8QUw2FZtFnKkELpAjAZcCytry8zs3zdtMlUCCUK+ou285k/1fGnvyIbW6kTgdURB5rD5rrencLYzE3rKyAaQ1BBZFl5QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYYLQV3a2WJJTpFkeTfFEK5RcCUmufgo/QRsHdS56zPrZH42gPIt6eY8qnSoWVHWjDQfalF0mkPyDZimbBfo9OUYY95EpV7jAcF/4vOMoglhJWVffXiAQ1SI6KAiE6YjyqwA== \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/svg/prepare.json b/crates/provenance/encypher/src/fixtures/prepared/svg/prepare.json new file mode 100644 index 0000000000..ccf50e959c --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/svg/prepare.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"18ec3a3b-1a2c-4c84-a43d-1e3da58a30ad","instance_id":"urn:uuid:52efe104-9f63-4a47-8610-0fc5796bb722","document_id":"doc_152b96ff8cfd2df92e7be175360181db","expires_at":"2026-11-02T02:41:31.073377Z","mime":"image/svg+xml","expected_manifest_size":37264,"effective_action":"c2pa.opened","geometry":{"disposition":"embedded","edits":[{"op":"insert_carrier","offset":83,"length":49778}],"carrier_span":[0,49778],"carrier_output_span":[83,49778],"expected_final_size":49958,"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[{"offset":83,"length":49778}]},"carrier_recipe":{"type":"standard"},"planner_options":{"tiff_placement":"main_ifd_when_single"}},"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/svg/recover.json b/crates/provenance/encypher/src/fixtures/prepared/svg/recover.json new file mode 100644 index 0000000000..6f6290ecd7 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/svg/recover.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff","prepare":{"protocol":"encypher-prepared-signing/2","plan_id":"18ec3a3b-1a2c-4c84-a43d-1e3da58a30ad","instance_id":"urn:uuid:52efe104-9f63-4a47-8610-0fc5796bb722","document_id":"doc_152b96ff8cfd2df92e7be175360181db","expires_at":"2026-11-02T02:41:31.073377Z","mime":"image/svg+xml","expected_manifest_size":37264,"effective_action":"c2pa.opened","geometry":{"edits":[{"op":"insert_carrier","length":49778,"offset":83}],"disposition":"embedded","carrier_span":[0,49778],"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[{"length":49778,"offset":83}]},"carrier_recipe":{"type":"standard"},"planner_options":{"tiff_placement":"main_ifd_when_single"},"carrier_output_span":[83,49778],"expected_final_size":49958},"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"},"finalize":{"protocol":"encypher-prepared-signing/2","plan_id":"18ec3a3b-1a2c-4c84-a43d-1e3da58a30ad","evidence_id":"ef0ab30a-2a24-49aa-b022-43b071b75084","instance_id":"urn:uuid:52efe104-9f63-4a47-8610-0fc5796bb722","document_id":"doc_152b96ff8cfd2df92e7be175360181db","asset_id":"doc_152b96ff8cfd2df92e7be175360181db","mime":"image/svg+xml","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"0f69308745f28354c4589c58e77e582d4fcfd45389fe1475b6fe3f9b7a530c0a","binding":{"type":"data_hash","digest":"0f69308745f28354c4589c58e77e582d4fcfd45389fe1475b6fe3f9b7a530c0a"},"manifest_store_b64":"AACRkGp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFqanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTozOWY2OTZjZC1lZDQwLTRjOWEtYjA5MC1jZDFhNGQ0ZTUyOGEAAAAFBWp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFgg0mZIYnhlJ3bxG0OKZxjETtEfKESULhQIKO4orvKNQXlyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhNwAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY18xNTJiOTZmZjhjZmQyZGY5MmU3YmUxNzUzNjAxODFkYmhpbWFnZV9pZHgkZG9jXzE1MmI5NmZmOGNmZDJkZjkyZTdiZTE3NTM2MDE4MWRiaXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMwWgAAAaVqdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFxY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEAwZjY5MzA4NzQ1ZjI4MzU0YzQ1ODljNThlNzdlNTgyZDRmY2ZkNDUzODlmZTE0NzViNmZlM2Y5YjdhNTMwYzBhaWRjOmZvcm1hdG1pbWFnZS9zdmcreG1sZGRhdGGlY3VybHiSaHR0cHM6Ly9hc3luYy1wcmVwYXJlZC1maXh0dXJlLnB1Ymxpc2hlci5leGFtcGxlLmNvbS9fZW5jeXBoZXIvcHJlcGFyYXRpb24vc291cmNlLzBmNjkzMDg3NDVmMjgzNTRjNDU4OWM1OGU3N2U1ODJkNGZjZmQ0NTM4OWZlMTQ3NWI2ZmUzZjliN2E1MzBjMGFjYWxnZnNoYTI1NmRoYXNoWCAPaTCHRfKDVMRYnFjnflgtT8/UU4n+FHW2/j+belMMCmlkYzpmb3JtYXRtaW1hZ2Uvc3ZnK3htbGRzaXplGLQAAACPanVtYgAAAChqdW1kY2JvcgARABCAAACqADibcQNjMnBhLmhhc2guZGF0YQAAAABfY2JvcqRqZXhjbHVzaW9uc4GiZXN0YXJ0GFNmbGVuZ3RoGcJyY2FsZ2ZzaGEyNTZkaGFzaFggD2kwh0Xyg1TEWJxY535YLU/P1FOJ/hR1tv4/m3pTDApjcGFkQAAAA4hqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADWWNib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOjUyZWZlMTA0LTlmNjMtNGE0Ny04NjEwLTBmYzU3OTZiYjcyMnRjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYTozOWY2OTZjZC1lZDQwLTRjOWEtYjA5MC1jZDFhNGQ0ZTUyOGEvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgghpKA0wW1j3Itz6C9P5VHO5QAc0JKktu56EfI33aI63BjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIGrNHEP2yWQbZmHTbx+oiiew3VciSJGJAPWDSsVDeMicY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFgg5wY94FNsaiKnqVgNbE5arlIgL1B/ZBRL7QCPyRq760JjYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFgg0mZIYnhlJ3bxG0OKZxjETtEfKESULhQIKO4orvKNQXljYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCAcfvgHstKZD8PealCtWiNHqxf/LabBa6MrZshI7tUx/mNhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGV4QDBmNjkzMDg3NDVmMjgzNTRjNDU4OWM1OGU3N2U1ODJkNGZjZmQ0NTM4OWZlMTQ3NWI2ZmUzZjliN2E1MzBjMGEAAIiOanVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAAIheY2JvctKEWQflogE4IhghglkD7TCCA+kwggNuoAMCAQICFBl/GL93F35s2XBg9wfGygQmz1VqMAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDMwMTQxMjlaMD8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKDBRFbmN5cGhlciBDb3Jwb3JhdGlvbjERMA8GA1UEAwwIRW5jeXBoZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ72e/ngX8g69guPOYHm/fOEhjXrEDb/spjt4t/35SbjTayHEkJlg5uOFaOplJGghhiic5ONmXcW/mdHJiY6SulTanfSRAqHuVfXRr/CsX0CMsfosSXwFeCPyhxHlvaQgSjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFAq4r/HhwLv8s4vT6dmsJq2gPRoKMB8GA1UdIwQYMBaAFGUMH+hj/pazcLbtUqen4KF4YNxJMDoGA1UdHwQzMDEwL6AtoCuGKWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL2lzc3VpbmctY2EuY3JsMHUGCCsGAQUFBwEBBGkwZzAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDwGCCsGAQUFBzAChjBodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvaXNzdWluZy1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMBkGCSsGAQQBg+heAwQMBgorBgEEAYPoXgMKMDMGCSsGAQQBg+heBAQmDCQwMTlkMjI0MS1lYjk3LTc3MjgtOWVjMC1jZGFhZmJhMzAwYzIwCgYIKoZIzj0EAwMDaQAwZgIxAPXvevd8+FKCy9hXvQRZHG4JYfgny0TY5bpYBP1T8i1a8YG/xWrbRIJITK1iMn0JogIxAPibTB2Pq8Ckvz/aNjG1X0h+bb13E615KT6qWh01Vu4Pyytj3/nTLkW1ZYou9OePYVkD6zCCA+cwggNuoAMCAQICFE99RbNxCOhQOov3zhDPhvHFG0swMAoGCCqGSM49BAMDMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSMwIQYDVQQDDBpFbmN5cGhlciBDMlBBIFJvb3QgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0zMTExMDMwMTQxMjlaMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOehmI2TPhJvCzHqbQre/SEVewkylgu9cxvQdh5UC8JulDQ4Eo8waJEr0upe3LJ0b4sQfIbfJ1udXSrgFKD4vEc8HlDAj7U9vCu0zXXjkw1/5HsNmPD0fRq3NXy3OswCuqOCAYYwggGCMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwHwYDVR0jBBgwFoAU6G5XZysFzIwLWHhxfLWRdN+5ah4wNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvcm9vdC1jYS5jcmwwcgYIKwYBBQUHAQEEZjBkMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwOQYIKwYBBQUHMAKGLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9yb290LWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwCgYIKoZIzj0EAwMDZwAwZAIwVyKy+PX6eryaIRifeEi5VgbOghUjEn5AAn2G2wHVq6Bf/lhO3tYfyY3X5xhcW3b8AjAuW4s4UIm2lljp/TeScxKBRCiuj4UTE8cGQJNt2S1rzFmntOP+Uo7OMGKn1DdunJCiZ3NpZ1RzdDKhaXRzdFRva2Vuc4GhY3ZhbFkGozCCBp8GCSqGSIb3DQEHAqCCBpAwggaMAgEDMQ8wDQYJYIZIAWUDBAICBQAwgYwGCyqGSIb3DQEJEAEEoH0EezB5AgEBBgorBgEEAYPoXgUBMDEwDQYJYIZIAWUDBAIBBQAEIF7FRfQpoFKoQfCwc0D8gOYBkDdIQ1H1JNKrLeJi2qiPAhQZEmOyFCm8t28wlbBWRoIicVpOZRgPMjAyNjExMDIwMTQxMzFaMAMCAQECCQCSlacBppkOHKCCA+wwggPoMIIDbqADAgECAhQoeKpdmqtzWZUbLWXhGy79mL0vizAKBggqhkjOPQQDAzCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMjAxNDEyOVowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xITAfBgNVBAMMGEVuY3lwaGVyIEMyUEEgVFNBIFNpZ25lcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABGv2BhCb52j9boQh4o3ZR/mr2fwGgeZGrANH9pOO/uaeyODk9gGmh6GenNSnqIUFYN7woDI5ONTH3tAPrQIm7YVQu6keogVN3kuvxMPb3TdL+8vc904sziaqEH+TKjGjhqOCAYQwggGAMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMB0GA1UdDgQWBBQy0HupUYSXYMQ3OaJOEocz1owQrzAfBgNVHSMEGDAWgBQZVh9Y111wrqkyEKYc8ye0t0KUSzBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMweQYIKwYBBQUHAQEEbTBrMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS90c2EtaXNzdWluZy1jYS5jcnQwPgYDVR0fBDcwNTAzoDGgL4YtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvdHNhLWlzc3VpbmctY2EuY3JsMAoGCCqGSM49BAMDA2gAMGUCMDGW0eZQ2bre4muVamkRdvMfxTre8s78q6IpmHs7JktYnvehwFkut3QgdLELPdKWfQIxAJiLhLjPA5fQWD3FCHr8MszLQ7qMidnEiKf44YJsPF2G14CivkTwsVCP9VDA2qQbPTGCAfUwggHxAgEBMIGuMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSowKAYDVQQDDCFFbmN5cGhlciBDMlBBIFRTQSBJc3N1aW5nIENBIDIwMjYCFCh4ql2aq3NZlRstZeEbLv2YvS+LMA0GCWCGSAFlAwQCAgUAoIG2MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAeBgkqhkiG9w0BCQUxERgPMjAyNjExMDIwMTQxMzFaMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIDxp/HaGi5RL9ideEDYibfB46z1dX4Gi4y7yJIR8TLBSMD8GCSqGSIb3DQEJBDEyBDCrygi1J3/yLIjzquVfO6HWunC12B+uIj4s0uSp98OeIT+helHFBpW2J2FCO+Ue00gwCgYIKoZIzj0EAwMEZzBlAjEAulDhFludpbrKPeNjNR+OAR9dkKjKDmO2ssxcX/BSDwNgsASre8QUw2FZtFnKkELpAjAZcCytry8zs3zdtMlUCCUK+ou285k/1fGnvyIbW6kTgdURB5rD5rrencLYzE3rKyAaQ1BBZFl5QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYYLQV3a2WJJTpFkeTfFEK5RcCUmufgo/QRsHdS56zPrZH42gPIt6eY8qnSoWVHWjDQfalF0mkPyDZimbBfo9OUYY95EpV7jAcF/4vOMoglhJWVffXiAQ1SI6KAiE6YjyqwA==","manifest_sha256":"6115eb178d5eefaf985f93c858ab0cca0d9f2b39780206efca5bb39903d6b974","carrier_sha256":"2f203a04cff50e55f5f6a9ffcef6e4bb179abb7faf0a8aa1f43d3be59b3eeae6","expected_final_size":49958,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_152b96ff8cfd2df92e7be175360181db","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"}} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/svg/source.svg b/crates/provenance/encypher/src/fixtures/prepared/svg/source.svg new file mode 100644 index 0000000000..d2c0e95e01 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/svg/source.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/webp/envelope.json b/crates/provenance/encypher/src/fixtures/prepared/webp/envelope.json new file mode 100644 index 0000000000..32f7f91f21 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/webp/envelope.json @@ -0,0 +1,89 @@ +{ + "deployment_id": "async-prepared-api-fixture", + "identity": "1d47e07ae1fc82f117dac16991e9b6b7c10af991fb7aa81d152e3b908f07eba7", + "item": { + "kind": "embedded", + "output_b64": "UklGRsaRAABXRUJQVlA4TCgAAAAvL8AHALkKRPQ/dlGKQvQ/oLZtFOhe2H9tagoCSVu87R+ESEB/vXjnQzJQQYqRAAAAAJGKanVtYgAAAB5qdW1kYzJwYQARABCAAACqADibcQNjMnBhAAAAkWRqdW1iAAAAR2p1bWRjMm1hABEAEIAAAKoAOJtxA3VybjpjMnBhOjIxMjJjZjAzLTBiNmEtNDU5OC1hNGQxLTBlZjVjMDRmNWM1ZgAAAAT/anVtYgAAAClqdW1kYzJhcwARABCAAACqADibcQNjMnBhLmFzc2VydGlvbnMAAAAA6mp1bWIAAAApanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5hY3Rpb25zLnYyAAAAALljYm9yomdhY3Rpb25zgaJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNjYWxnZnNoYTI1NmRoYXNoWCAWyxTntr6Kk9TWP2dcx1fPg3IwV1OTBoh+2v4IZGYXcnJhbGxBY3Rpb25zSW5jbHVkZWT1AAAAt2p1bWIAAAAtanVtZGNib3IAEQAQgAAAqgA4m3EDb3JnLmVuY3lwaGVyLnN0YXR1cwAAAACCY2JvcqJ0c3RhdHVzTGlzdENyZWRlbnRpYWx4UGh0dHBzOi8vdmVyaWZ5LmVuY3lwaGVyLmNvbS9zdGF0dXMvdjEvbGlzdHMvYmNmZTAwMTQtOTVlYS00YjYxLWJkY2EtODMwMTk1YTQ0Y2Q1b3N0YXR1c0xpc3RJbmRleGE1AAAA/2p1bWIAAAAxanVtZGNib3IAEQAQgAAAqgA4m3EDY29tLmVuY3lwaGVyLnByb3ZlbmFuY2UAAAAAxmNib3Klb29yZ2FuaXphdGlvbl9pZHRvcmdfY2RuXzQ4M2RhZjUwZDY0NWlzaWduZXJfaWRtZW5jeXBoZXIuZnJlZWtkb2N1bWVudF9pZHgkZG9jXzIxN2ZhMTRiODMzMzExMzFkZmRhZWIxZGQ3N2E4OGY0aGltYWdlX2lkeCRkb2NfMjE3ZmExNGI4MzMzMTEzMWRmZGFlYjFkZDc3YTg4ZjRpc2lnbmVkX2F0dDIwMjYtMTEtMDJUMDE6NDE6MzBaAAABn2p1bWIAAAAsanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5pbmdyZWRpZW50LnYzAAAAAWtjYm9ypGxyZWxhdGlvbnNoaXBocGFyZW50T2ZoZGM6dGl0bGV4QGFiYzI4NmQ2NWQ0YTgyM2JhYjA0NmQ0OWVjNWEzOTQ5ZDdiZDE2OGYyYmMyMGY1MjA5ZDI3NTg0YzZlNTVmMWJpZGM6Zm9ybWF0amltYWdlL3dlYnBkZGF0YaVjdXJseJJodHRwczovL2FzeW5jLXByZXBhcmVkLWZpeHR1cmUucHVibGlzaGVyLmV4YW1wbGUuY29tL19lbmN5cGhlci9wcmVwYXJhdGlvbi9zb3VyY2UvYWJjMjg2ZDY1ZDRhODIzYmFiMDQ2ZDQ5ZWM1YTM5NDlkN2JkMTY4ZjJiYzIwZjUyMDlkMjc1ODRjNmU1NWYxYmNhbGdmc2hhMjU2ZGhhc2hYIKvChtZdSoI7qwRtSexaOUnXvRaPK8IPUgnSdYTG5V8baWRjOmZvcm1hdGppbWFnZS93ZWJwZHNpemUYPAAAAI9qdW1iAAAAKGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaGFzaC5kYXRhAAAAAF9jYm9ypGpleGNsdXNpb25zgaJlc3RhcnQYPGZsZW5ndGgZkZJjYWxnZnNoYTI1NmRoYXNoWCBnsGEwhciZguuI1n8svFbtUiWcztWLMc4uaOZKMtq0EGNwYWRAAAADiGp1bWIAAAAnanVtZGMyY2wAEQAQgAAAqgA4m3EDYzJwYS5jbGFpbS52MgAAAANZY2JvcqZqaW5zdGFuY2VJRHgtdXJuOnV1aWQ6OTJkYTBlMWYtZGY0NS00ZDcyLWFjMmEtMzk1MTNjNmIyM2M5dGNsYWltX2dlbmVyYXRvcl9pbmZvo2RuYW1leBtFbmN5cGhlciBDRE4gUHJvdmVuYW5jZS8xLjBndmVyc2lvbmMxLjBrc3BlY1ZlcnNpb25lMi40LjBpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjIxMjJjZjAzLTBiNmEtNDU5OC1hNGQxLTBlZjVjMDRmNWM1Zi9jMnBhLnNpZ25hdHVyZXJjcmVhdGVkX2Fzc2VydGlvbnOFo2N1cmx4KnNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuYWN0aW9ucy52MmRoYXNoWCCV2FlI4cTI4ABIGA/Ax8oRZLELRK2paoxu6igVPYVk3GNhbGdmc2hhMjU2o2N1cmx4LnNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL29yZy5lbmN5cGhlci5zdGF0dXNkaGFzaFggTIi4N16MBC8etbksqVikDgRWr7NZ2bjFv+YQF+VH95xjYWxnZnNoYTI1NqNjdXJseDJzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jb20uZW5jeXBoZXIucHJvdmVuYW5jZWRoYXNoWCB1f0fuprPoa7wFgtkHhqKNIb8zCd4V+p599MmmNPpgO2NhbGdmc2hhMjU2o2N1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCAWyxTntr6Kk9TWP2dcx1fPg3IwV1OTBoh+2v4IZGYXcmNhbGdmc2hhMjU2o2N1cmx4KXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaGFzaC5kYXRhZGhhc2hYIKaXmWVbI16UofojLUlRTPCCcq4VHb3jhInCIePc7L/XY2FsZ2ZzaGEyNTZjYWxnZnNoYTI1NmhkYzp0aXRsZXhAYWJjMjg2ZDY1ZDRhODIzYmFiMDQ2ZDQ5ZWM1YTM5NDlkN2JkMTY4ZjJiYzIwZjUyMDlkMjc1ODRjNmU1NWYxYgAAiI5qdW1iAAAAKGp1bWRjMmNzABEAEIAAAKoAOJtxA2MycGEuc2lnbmF0dXJlAAAAiF5jYm9y0oRZB+WiATgiGCGCWQPtMIID6TCCA26gAwIBAgIUGX8Yv3cXfmzZcGD3B8bKBCbPVWowCgYIKoZIzj0EAwMwgZExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAkBgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMzAxNDEyOVowPzELMAkGA1UEBhMCVVMxHTAbBgNVBAoMFEVuY3lwaGVyIENvcnBvcmF0aW9uMREwDwYDVQQDDAhFbmN5cGhlcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABDvZ7+eBfyDr2C485geb984SGNesQNv+ymO3i3/flJuNNrIcSQmWDm44Vo6mUkaCGGKJzk42Zdxb+Z0cmJjpK6VNqd9JECoe5V9dGv8KxfQIyx+ixJfAV4I/KHEeW9pCBKOCAdYwggHSMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUCriv8eHAu/yzi9Pp2awmraA9GgowHwYDVR0jBBgwFoAUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvaXNzdWluZy1jYS5jcmwwdQYIKwYBBQUHAQEEaTBnMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwPAYIKwYBBQUHMAKGMGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9pc3N1aW5nLWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwGQYJKwYBBAGD6F4DBAwGCisGAQQBg+heAwowMwYJKwYBBAGD6F4EBCYMJDAxOWQyMjQxLWViOTctNzcyOC05ZWMwLWNkYWFmYmEzMDBjMjAKBggqhkjOPQQDAwNpADBmAjEA9e9693z4UoLL2Fe9BFkcbglh+CfLRNjlulgE/VPyLVrxgb/FattEgkhMrWIyfQmiAjEA+JtMHY+rwKS/P9o2MbVfSH5tvXcTrXkpPqpaHTVW7g/LK2Pf+dMuRbVlii70549hWQPrMIID5zCCA26gAwIBAgIUT31Fs3EI6FA6i/fOEM+G8cUbSzAwCgYIKoZIzj0EAwMwgY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xIzAhBgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTMxMTEwMzAxNDEyOVowgZExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAkBgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE56GYjZM+Em8LMeptCt79IRV7CTKWC71zG9B2HlQLwm6UNDgSjzBokSvS6l7csnRvixB8ht8nW51dKuAUoPi8RzweUMCPtT28K7TNdeOTDX/kew2Y8PR9Grc1fLc6zAK6o4IBhjCCAYIwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lBBkwFwYKKwYBBAGD6F4CAQYJKoZIhvcvAQEFMB0GA1UdDgQWBBRlDB/oY/6Ws3C27VKnp+CheGDcSTAfBgNVHSMEGDAWgBTobldnKwXMjAtYeHF8tZF037lqHjA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC9yb290LWNhLmNybDByBggrBgEFBQcBAQRmMGQwJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDA5BggrBgEFBQcwAoYtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L3Jvb3QtY2EuY3J0ME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczAKBggqhkjOPQQDAwNnADBkAjBXIrL49fp6vJohGJ94SLlWBs6CFSMSfkACfYbbAdWroF/+WE7e1h/JjdfnGFxbdvwCMC5bizhQibaWWOn9N5JzEoFEKK6PhRMTxwZAk23ZLWvMWae04/5Sjs4wYqfUN26ckKJnc2lnVHN0MqFpdHN0VG9rZW5zgaFjdmFsWQajMIIGnwYJKoZIhvcNAQcCoIIGkDCCBowCAQMxDzANBglghkgBZQMEAgIFADCBjAYLKoZIhvcNAQkQAQSgfQR7MHkCAQEGCisGAQQBg+heBQEwMTANBglghkgBZQMEAgEFAAQgyMa9/A1stIFIb8Z86wpFoCKsAvkUhc9igCN7Vl8bR2kCFD6lBm1zvqIpMy7NcLKJAz2JzEFFGA8yMDI2MTEwMjAxNDEzMFowAwIBAQIJANB9N+ZysjR4oIID7DCCA+gwggNuoAMCAQICFCh4ql2aq3NZlRstZeEbLv2YvS+LMAoGCCqGSM49BAMDMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSowKAYDVQQDDCFFbmN5cGhlciBDMlBBIFRTQSBJc3N1aW5nIENBIDIwMjYwHhcNMjYxMTAyMDE0MTI5WhcNMjcxMTAyMDE0MTI5WjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEhMB8GA1UEAwwYRW5jeXBoZXIgQzJQQSBUU0EgU2lnbmVyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEa/YGEJvnaP1uhCHijdlH+avZ/AaB5kasA0f2k47+5p7I4OT2AaaHoZ6c1KeohQVg3vCgMjk41Mfe0A+tAibthVC7qR6iBU3eS6/Ew9vdN0v7y9z3TizOJqoQf5MqMaOGo4IBhDCCAYAwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwHQYDVR0OBBYEFDLQe6lRhJdgxDc5ok4ShzPWjBCvMB8GA1UdIwQYMBaAFBlWH1jXXXCuqTIQphzzJ7S3QpRLME0GA1UdIARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczB5BggrBgEFBQcBAQRtMGswJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDBABggrBgEFBQcwAoY0aHR0cDovL2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L3RzYS1pc3N1aW5nLWNhLmNydDA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC90c2EtaXNzdWluZy1jYS5jcmwwCgYIKoZIzj0EAwMDaAAwZQIwMZbR5lDZut7ia5VqaRF28x/FOt7yzvyroimYezsmS1ie96HAWS63dCB0sQs90pZ9AjEAmIuEuM8Dl9BYPcUIevwyzMtDuoyJ2cSIp/jhgmw8XYbXgKK+RPCxUI/1UMDapBs9MYIB9TCCAfECAQEwga4wgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAoBgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNgIUKHiqXZqrc1mVGy1l4Rsu/Zi9L4swDQYJYIZIAWUDBAICBQCggbYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMB4GCSqGSIb3DQEJBTERGA8yMDI2MTEwMjAxNDEzMFowNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgPGn8doaLlEv2J14QNiJt8HjrPV1fgaLjLvIkhHxMsFIwPwYJKoZIhvcNAQkEMTIEMCH8Gat9fPVde07NLu2+Jx6/XHZswkmH7lawepfqcsWUWMAL03T+26aQ/t3TwWakvzAKBggqhkjOPQQDAwRnMGUCMFEd935+nhUn7JFNTwwDssO6GQSTv2vTf0+HSZJeMI20GYyuFAKOeYHJR0AGk+O8yAIxANhJab8oaxMKW2+QpHDnChheIHqv0btTE+8ic2zZYvUWoVqvJV6rG8pCe4IjyPlvDxpDUEFkWXlBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9lhgWB6LT2m5Vx2qcAJLpSAu0BPDeHd6Kcl0R3HmdVr0dc266B3P0yey/xVuK0bpLUSFOAF3w909EIm2bsES3PmX2E6EQomKeDLP1t7xa6yzYDJJecxCB0GVKnklAI7r+lQy", + "planned": { + "digest": "67b0613085c89982eb88d67f2cbc56ed52259cced58b31ce2e68e64a32dab410", + "geometry": { + "binding_recipe": { + "algorithm": "sha256", + "exclusions": [ + { + "length": 37266, + "offset": 60 + } + ], + "type": "data_hash" + }, + "carrier_output_span": [ + 60, + 37266 + ], + "carrier_recipe": { + "type": "standard" + }, + "carrier_span": [ + 0, + 37266 + ], + "disposition": "embedded", + "edits": [ + { + "b64": "xpEAAA==", + "offset": 4, + "op": "patch" + }, + { + "length": 37266, + "offset": 60, + "op": "insert_carrier" + } + ], + "expected_final_size": 37326, + "planner_options": { + "tiff_placement": "main_ifd_when_single" + } + }, + "idempotency_key": "async-prepared-fixture:webp:14daf9fd8ccc45ae8d87d190129b9846", + "plan_id": "29fe90ad-757b-4321-93f0-128f4f61aa6d", + "release": { + "abi": 1, + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "release": 18, + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f", + "signature": "33Dab6p1n5yJpIbXypes+yr6xXalcFonsh5h9YaFzMo18zFMHlNDXzriZNOe5LHZQ03P7Ki0T4ZXT+QNS6okCw==" + }, + "request": { + "carrier_requirement": "embedded_required", + "contract_version": 2, + "expected_policy_digest": "171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff", + "page_url": "https://async-prepared-fixture.publisher.example.com/article", + "probe": { + "planner_id": "05079ae09f9fc9af0b97683ee52c3fea6e6be0c30c993eb6d30dac36ec811a39", + "sha256": "eb0c58b5178500a1fd08e351ab2237582f5a9aedad71871f3580ea722ad10a6f" + }, + "protocol": "encypher-prepared-signing/2", + "source": { + "byte_length": 60, + "mime": "image/webp", + "origin_host": "async-prepared-fixture.publisher.example.com", + "reference": "https://async-prepared-fixture.publisher.example.com/_encypher/preparation/source/abc286d65d4a823bab046d49ec5a3949d7bd168f2bc20f5209d27584c6e55f1b", + "reference_kind": "publisher_snapshot", + "sha256": "abc286d65d4a823bab046d49ec5a3949d7bd168f2bc20f5209d27584c6e55f1b" + }, + "structure": [ + { + "b64": "UklGRjQAAABXRUJQVlA4TCgAAAA=", + "offset": 0 + } + ] + } + }, + "source_b64": "UklGRjQAAABXRUJQVlA4TCgAAAAvL8AHALkKRPQ/dlGKQvQ/oLZtFOhe2H9tagoCSVu87R+ESEB/vXjn" + }, + "protocol": "encypher-preparation/1", + "retirement_epoch": 0, + "variant": "" +} diff --git a/crates/provenance/encypher/src/fixtures/prepared/webp/finalize.json b/crates/provenance/encypher/src/fixtures/prepared/webp/finalize.json new file mode 100644 index 0000000000..38fa675078 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/webp/finalize.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"29fe90ad-757b-4321-93f0-128f4f61aa6d","evidence_id":"73947fcf-88ba-4e3d-b696-fb14ee442448","instance_id":"urn:uuid:92da0e1f-df45-4d72-ac2a-39513c6b23c9","document_id":"doc_217fa14b83331131dfdaeb1dd77a88f4","asset_id":"doc_217fa14b83331131dfdaeb1dd77a88f4","mime":"image/webp","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"67b0613085c89982eb88d67f2cbc56ed52259cced58b31ce2e68e64a32dab410","binding":{"type":"data_hash","digest":"67b0613085c89982eb88d67f2cbc56ed52259cced58b31ce2e68e64a32dab410"},"manifest_store_b64":"AACRimp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFkanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToyMTIyY2YwMy0wYjZhLTQ1OTgtYTRkMS0wZWY1YzA0ZjVjNWYAAAAE/2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFggFssU57a+ipPU1j9nXMdXz4NyMFdTkwaIftr+CGRmF3JyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhNQAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY18yMTdmYTE0YjgzMzMxMTMxZGZkYWViMWRkNzdhODhmNGhpbWFnZV9pZHgkZG9jXzIxN2ZhMTRiODMzMzExMzFkZmRhZWIxZGQ3N2E4OGY0aXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMwWgAAAZ9qdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFrY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEBhYmMyODZkNjVkNGE4MjNiYWIwNDZkNDllYzVhMzk0OWQ3YmQxNjhmMmJjMjBmNTIwOWQyNzU4NGM2ZTU1ZjFiaWRjOmZvcm1hdGppbWFnZS93ZWJwZGRhdGGlY3VybHiSaHR0cHM6Ly9hc3luYy1wcmVwYXJlZC1maXh0dXJlLnB1Ymxpc2hlci5leGFtcGxlLmNvbS9fZW5jeXBoZXIvcHJlcGFyYXRpb24vc291cmNlL2FiYzI4NmQ2NWQ0YTgyM2JhYjA0NmQ0OWVjNWEzOTQ5ZDdiZDE2OGYyYmMyMGY1MjA5ZDI3NTg0YzZlNTVmMWJjYWxnZnNoYTI1NmRoYXNoWCCrwobWXUqCO6sEbUnsWjlJ170WjyvCD1IJ0nWExuVfG2lkYzpmb3JtYXRqaW1hZ2Uvd2VicGRzaXplGDwAAACPanVtYgAAAChqdW1kY2JvcgARABCAAACqADibcQNjMnBhLmhhc2guZGF0YQAAAABfY2JvcqRqZXhjbHVzaW9uc4GiZXN0YXJ0GDxmbGVuZ3RoGZGSY2FsZ2ZzaGEyNTZkaGFzaFggZ7BhMIXImYLriNZ/LLxW7VIlnM7VizHOLmjmSjLatBBjcGFkQAAAA4hqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADWWNib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOjkyZGEwZTFmLWRmNDUtNGQ3Mi1hYzJhLTM5NTEzYzZiMjNjOXRjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYToyMTIyY2YwMy0wYjZhLTQ1OTgtYTRkMS0wZWY1YzA0ZjVjNWYvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggldhZSOHEyOAASBgPwMfKEWSxC0StqWqMbuooFT2FZNxjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIEyIuDdejAQvHrW5LKlYpA4EVq+zWdm4xb/mEBflR/ecY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFggdX9H7qaz6Gu8BYLZB4aijSG/MwneFfqeffTJpjT6YDtjYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggFssU57a+ipPU1j9nXMdXz4NyMFdTkwaIftr+CGRmF3JjYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCCml5llWyNelKH6Iy1JUUzwgnKuFR2944SJwiHj3Oy/12NhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGV4QGFiYzI4NmQ2NWQ0YTgyM2JhYjA0NmQ0OWVjNWEzOTQ5ZDdiZDE2OGYyYmMyMGY1MjA5ZDI3NTg0YzZlNTVmMWIAAIiOanVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAAIheY2JvctKEWQflogE4IhghglkD7TCCA+kwggNuoAMCAQICFBl/GL93F35s2XBg9wfGygQmz1VqMAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDMwMTQxMjlaMD8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKDBRFbmN5cGhlciBDb3Jwb3JhdGlvbjERMA8GA1UEAwwIRW5jeXBoZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ72e/ngX8g69guPOYHm/fOEhjXrEDb/spjt4t/35SbjTayHEkJlg5uOFaOplJGghhiic5ONmXcW/mdHJiY6SulTanfSRAqHuVfXRr/CsX0CMsfosSXwFeCPyhxHlvaQgSjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFAq4r/HhwLv8s4vT6dmsJq2gPRoKMB8GA1UdIwQYMBaAFGUMH+hj/pazcLbtUqen4KF4YNxJMDoGA1UdHwQzMDEwL6AtoCuGKWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL2lzc3VpbmctY2EuY3JsMHUGCCsGAQUFBwEBBGkwZzAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDwGCCsGAQUFBzAChjBodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvaXNzdWluZy1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMBkGCSsGAQQBg+heAwQMBgorBgEEAYPoXgMKMDMGCSsGAQQBg+heBAQmDCQwMTlkMjI0MS1lYjk3LTc3MjgtOWVjMC1jZGFhZmJhMzAwYzIwCgYIKoZIzj0EAwMDaQAwZgIxAPXvevd8+FKCy9hXvQRZHG4JYfgny0TY5bpYBP1T8i1a8YG/xWrbRIJITK1iMn0JogIxAPibTB2Pq8Ckvz/aNjG1X0h+bb13E615KT6qWh01Vu4Pyytj3/nTLkW1ZYou9OePYVkD6zCCA+cwggNuoAMCAQICFE99RbNxCOhQOov3zhDPhvHFG0swMAoGCCqGSM49BAMDMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSMwIQYDVQQDDBpFbmN5cGhlciBDMlBBIFJvb3QgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0zMTExMDMwMTQxMjlaMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOehmI2TPhJvCzHqbQre/SEVewkylgu9cxvQdh5UC8JulDQ4Eo8waJEr0upe3LJ0b4sQfIbfJ1udXSrgFKD4vEc8HlDAj7U9vCu0zXXjkw1/5HsNmPD0fRq3NXy3OswCuqOCAYYwggGCMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwHwYDVR0jBBgwFoAU6G5XZysFzIwLWHhxfLWRdN+5ah4wNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvcm9vdC1jYS5jcmwwcgYIKwYBBQUHAQEEZjBkMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwOQYIKwYBBQUHMAKGLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9yb290LWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwCgYIKoZIzj0EAwMDZwAwZAIwVyKy+PX6eryaIRifeEi5VgbOghUjEn5AAn2G2wHVq6Bf/lhO3tYfyY3X5xhcW3b8AjAuW4s4UIm2lljp/TeScxKBRCiuj4UTE8cGQJNt2S1rzFmntOP+Uo7OMGKn1DdunJCiZ3NpZ1RzdDKhaXRzdFRva2Vuc4GhY3ZhbFkGozCCBp8GCSqGSIb3DQEHAqCCBpAwggaMAgEDMQ8wDQYJYIZIAWUDBAICBQAwgYwGCyqGSIb3DQEJEAEEoH0EezB5AgEBBgorBgEEAYPoXgUBMDEwDQYJYIZIAWUDBAIBBQAEIMjGvfwNbLSBSG/GfOsKRaAirAL5FIXPYoAje1ZfG0dpAhQ+pQZtc76iKTMuzXCyiQM9icxBRRgPMjAyNjExMDIwMTQxMzBaMAMCAQECCQDQfTfmcrI0eKCCA+wwggPoMIIDbqADAgECAhQoeKpdmqtzWZUbLWXhGy79mL0vizAKBggqhkjOPQQDAzCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMjAxNDEyOVowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xITAfBgNVBAMMGEVuY3lwaGVyIEMyUEEgVFNBIFNpZ25lcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABGv2BhCb52j9boQh4o3ZR/mr2fwGgeZGrANH9pOO/uaeyODk9gGmh6GenNSnqIUFYN7woDI5ONTH3tAPrQIm7YVQu6keogVN3kuvxMPb3TdL+8vc904sziaqEH+TKjGjhqOCAYQwggGAMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMB0GA1UdDgQWBBQy0HupUYSXYMQ3OaJOEocz1owQrzAfBgNVHSMEGDAWgBQZVh9Y111wrqkyEKYc8ye0t0KUSzBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMweQYIKwYBBQUHAQEEbTBrMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS90c2EtaXNzdWluZy1jYS5jcnQwPgYDVR0fBDcwNTAzoDGgL4YtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvdHNhLWlzc3VpbmctY2EuY3JsMAoGCCqGSM49BAMDA2gAMGUCMDGW0eZQ2bre4muVamkRdvMfxTre8s78q6IpmHs7JktYnvehwFkut3QgdLELPdKWfQIxAJiLhLjPA5fQWD3FCHr8MszLQ7qMidnEiKf44YJsPF2G14CivkTwsVCP9VDA2qQbPTGCAfUwggHxAgEBMIGuMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSowKAYDVQQDDCFFbmN5cGhlciBDMlBBIFRTQSBJc3N1aW5nIENBIDIwMjYCFCh4ql2aq3NZlRstZeEbLv2YvS+LMA0GCWCGSAFlAwQCAgUAoIG2MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAeBgkqhkiG9w0BCQUxERgPMjAyNjExMDIwMTQxMzBaMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIDxp/HaGi5RL9ideEDYibfB46z1dX4Gi4y7yJIR8TLBSMD8GCSqGSIb3DQEJBDEyBDAh/BmrfXz1XXtOzS7tvicev1x2bMJJh+5WsHqX6nLFlFjAC9N0/tumkP7d08FmpL8wCgYIKoZIzj0EAwMEZzBlAjBRHfd+fp4VJ+yRTU8MA7LDuhkEk79r039Ph0mSXjCNtBmMrhQCjnmByUdABpPjvMgCMQDYSWm/KGsTCltvkKRw5woYXiB6r9G7UxPvInNs2WL1FqFaryVeqxvKQnuCI8j5bw8aQ1BBZFl5QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYYFgei09puVcdqnACS6UgLtATw3h3einJdEdx5nVa9HXNuugdz9Mnsv8VbitG6S1EhTgBd8PdPRCJtm7BEtz5l9hOhEKJingyz9be8Wuss2AySXnMQgdBlSp5JQCO6/pUMg==","manifest_sha256":"5b153eab95513224c7a3028aa49b478e4313b4aabf8069e8b91c100584547b9e","carrier_sha256":"d254e0a642933492db5b05dfb56e2fab848b56d6211962f014ade509f07f738c","expected_final_size":37326,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_217fa14b83331131dfdaeb1dd77a88f4","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/webp/output.webp b/crates/provenance/encypher/src/fixtures/prepared/webp/output.webp new file mode 100644 index 0000000000..7c8e6b9101 Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/webp/output.webp differ diff --git a/crates/provenance/encypher/src/fixtures/prepared/webp/prepare.json b/crates/provenance/encypher/src/fixtures/prepared/webp/prepare.json new file mode 100644 index 0000000000..6219e60d3f --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/webp/prepare.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","plan_id":"29fe90ad-757b-4321-93f0-128f4f61aa6d","instance_id":"urn:uuid:92da0e1f-df45-4d72-ac2a-39513c6b23c9","document_id":"doc_217fa14b83331131dfdaeb1dd77a88f4","expires_at":"2026-11-02T02:41:30.648197Z","mime":"image/webp","expected_manifest_size":37258,"effective_action":"c2pa.opened","geometry":{"disposition":"embedded","edits":[{"op":"patch","offset":4,"b64":"xpEAAA=="},{"op":"insert_carrier","offset":60,"length":37266}],"carrier_span":[0,37266],"carrier_output_span":[60,37266],"expected_final_size":37326,"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[{"offset":60,"length":37266}]},"carrier_recipe":{"type":"standard"},"planner_options":{"tiff_placement":"main_ifd_when_single"}},"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/webp/recover.json b/crates/provenance/encypher/src/fixtures/prepared/webp/recover.json new file mode 100644 index 0000000000..0de2f4f6b0 --- /dev/null +++ b/crates/provenance/encypher/src/fixtures/prepared/webp/recover.json @@ -0,0 +1 @@ +{"protocol":"encypher-prepared-signing/2","policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff","prepare":{"protocol":"encypher-prepared-signing/2","plan_id":"29fe90ad-757b-4321-93f0-128f4f61aa6d","instance_id":"urn:uuid:92da0e1f-df45-4d72-ac2a-39513c6b23c9","document_id":"doc_217fa14b83331131dfdaeb1dd77a88f4","expires_at":"2026-11-02T02:41:30.648197Z","mime":"image/webp","expected_manifest_size":37258,"effective_action":"c2pa.opened","geometry":{"edits":[{"op":"patch","b64":"xpEAAA==","offset":4},{"op":"insert_carrier","length":37266,"offset":60}],"disposition":"embedded","carrier_span":[0,37266],"binding_recipe":{"type":"data_hash","algorithm":"sha256","exclusions":[{"length":37266,"offset":60}]},"carrier_recipe":{"type":"standard"},"planner_options":{"tiff_placement":"main_ifd_when_single"},"carrier_output_span":[60,37266],"expected_final_size":37326},"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"},"finalize":{"protocol":"encypher-prepared-signing/2","plan_id":"29fe90ad-757b-4321-93f0-128f4f61aa6d","evidence_id":"73947fcf-88ba-4e3d-b696-fb14ee442448","instance_id":"urn:uuid:92da0e1f-df45-4d72-ac2a-39513c6b23c9","document_id":"doc_217fa14b83331131dfdaeb1dd77a88f4","asset_id":"doc_217fa14b83331131dfdaeb1dd77a88f4","mime":"image/webp","content_hash_kind":"hard_binding","binding_algorithm":"c2pa.hash.data","binding_commitment":"67b0613085c89982eb88d67f2cbc56ed52259cced58b31ce2e68e64a32dab410","binding":{"type":"data_hash","digest":"67b0613085c89982eb88d67f2cbc56ed52259cced58b31ce2e68e64a32dab410"},"manifest_store_b64":"AACRimp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAJFkanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToyMTIyY2YwMy0wYjZhLTQ1OTgtYTRkMS0wZWY1YzA0ZjVjNWYAAAAE/2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAOpqdW1iAAAAKWp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucy52MgAAAAC5Y2JvcqJnYWN0aW9uc4GiZmFjdGlvbmtjMnBhLm9wZW5lZGpwYXJhbWV0ZXJzoWtpbmdyZWRpZW50c4GjY3VybHgtc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5pbmdyZWRpZW50LnYzY2FsZ2ZzaGEyNTZkaGFzaFggFssU57a+ipPU1j9nXMdXz4NyMFdTkwaIftr+CGRmF3JyYWxsQWN0aW9uc0luY2x1ZGVk9QAAALdqdW1iAAAALWp1bWRjYm9yABEAEIAAAKoAOJtxA29yZy5lbmN5cGhlci5zdGF0dXMAAAAAgmNib3KidHN0YXR1c0xpc3RDcmVkZW50aWFseFBodHRwczovL3ZlcmlmeS5lbmN5cGhlci5jb20vc3RhdHVzL3YxL2xpc3RzL2JjZmUwMDE0LTk1ZWEtNGI2MS1iZGNhLTgzMDE5NWE0NGNkNW9zdGF0dXNMaXN0SW5kZXhhNQAAAP9qdW1iAAAAMWp1bWRjYm9yABEAEIAAAKoAOJtxA2NvbS5lbmN5cGhlci5wcm92ZW5hbmNlAAAAAMZjYm9ypW9vcmdhbml6YXRpb25faWR0b3JnX2Nkbl80ODNkYWY1MGQ2NDVpc2lnbmVyX2lkbWVuY3lwaGVyLmZyZWVrZG9jdW1lbnRfaWR4JGRvY18yMTdmYTE0YjgzMzMxMTMxZGZkYWViMWRkNzdhODhmNGhpbWFnZV9pZHgkZG9jXzIxN2ZhMTRiODMzMzExMzFkZmRhZWIxZGQ3N2E4OGY0aXNpZ25lZF9hdHQyMDI2LTExLTAyVDAxOjQxOjMwWgAAAZ9qdW1iAAAALGp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuaW5ncmVkaWVudC52MwAAAAFrY2JvcqRscmVsYXRpb25zaGlwaHBhcmVudE9maGRjOnRpdGxleEBhYmMyODZkNjVkNGE4MjNiYWIwNDZkNDllYzVhMzk0OWQ3YmQxNjhmMmJjMjBmNTIwOWQyNzU4NGM2ZTU1ZjFiaWRjOmZvcm1hdGppbWFnZS93ZWJwZGRhdGGlY3VybHiSaHR0cHM6Ly9hc3luYy1wcmVwYXJlZC1maXh0dXJlLnB1Ymxpc2hlci5leGFtcGxlLmNvbS9fZW5jeXBoZXIvcHJlcGFyYXRpb24vc291cmNlL2FiYzI4NmQ2NWQ0YTgyM2JhYjA0NmQ0OWVjNWEzOTQ5ZDdiZDE2OGYyYmMyMGY1MjA5ZDI3NTg0YzZlNTVmMWJjYWxnZnNoYTI1NmRoYXNoWCCrwobWXUqCO6sEbUnsWjlJ170WjyvCD1IJ0nWExuVfG2lkYzpmb3JtYXRqaW1hZ2Uvd2VicGRzaXplGDwAAACPanVtYgAAAChqdW1kY2JvcgARABCAAACqADibcQNjMnBhLmhhc2guZGF0YQAAAABfY2JvcqRqZXhjbHVzaW9uc4GiZXN0YXJ0GDxmbGVuZ3RoGZGSY2FsZ2ZzaGEyNTZkaGFzaFggZ7BhMIXImYLriNZ/LLxW7VIlnM7VizHOLmjmSjLatBBjcGFkQAAAA4hqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAADWWNib3Kmamluc3RhbmNlSUR4LXVybjp1dWlkOjkyZGEwZTFmLWRmNDUtNGQ3Mi1hYzJhLTM5NTEzYzZiMjNjOXRjbGFpbV9nZW5lcmF0b3JfaW5mb6NkbmFtZXgbRW5jeXBoZXIgQ0ROIFByb3ZlbmFuY2UvMS4wZ3ZlcnNpb25jMS4wa3NwZWNWZXJzaW9uZTIuNC4waXNpZ25hdHVyZXhNc2VsZiNqdW1iZj0vYzJwYS91cm46YzJwYToyMTIyY2YwMy0wYjZhLTQ1OTgtYTRkMS0wZWY1YzA0ZjVjNWYvYzJwYS5zaWduYXR1cmVyY3JlYXRlZF9hc3NlcnRpb25zhaNjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggldhZSOHEyOAASBgPwMfKEWSxC0StqWqMbuooFT2FZNxjYWxnZnNoYTI1NqNjdXJseC5zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9vcmcuZW5jeXBoZXIuc3RhdHVzZGhhc2hYIEyIuDdejAQvHrW5LKlYpA4EVq+zWdm4xb/mEBflR/ecY2FsZ2ZzaGEyNTajY3VybHgyc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvY29tLmVuY3lwaGVyLnByb3ZlbmFuY2VkaGFzaFggdX9H7qaz6Gu8BYLZB4aijSG/MwneFfqeffTJpjT6YDtjYWxnZnNoYTI1NqNjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggFssU57a+ipPU1j9nXMdXz4NyMFdTkwaIftr+CGRmF3JjYWxnZnNoYTI1NqNjdXJseClzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmhhc2guZGF0YWRoYXNoWCCml5llWyNelKH6Iy1JUUzwgnKuFR2944SJwiHj3Oy/12NhbGdmc2hhMjU2Y2FsZ2ZzaGEyNTZoZGM6dGl0bGV4QGFiYzI4NmQ2NWQ0YTgyM2JhYjA0NmQ0OWVjNWEzOTQ5ZDdiZDE2OGYyYmMyMGY1MjA5ZDI3NTg0YzZlNTVmMWIAAIiOanVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAAIheY2JvctKEWQflogE4IhghglkD7TCCA+kwggNuoAMCAQICFBl/GL93F35s2XBg9wfGygQmz1VqMAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0yNzExMDMwMTQxMjlaMD8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKDBRFbmN5cGhlciBDb3Jwb3JhdGlvbjERMA8GA1UEAwwIRW5jeXBoZXIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ72e/ngX8g69guPOYHm/fOEhjXrEDb/spjt4t/35SbjTayHEkJlg5uOFaOplJGghhiic5ONmXcW/mdHJiY6SulTanfSRAqHuVfXRr/CsX0CMsfosSXwFeCPyhxHlvaQgSjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAgBgNVHSUEGTAXBgorBgEEAYPoXgIBBgkqhkiG9y8BAQUwHQYDVR0OBBYEFAq4r/HhwLv8s4vT6dmsJq2gPRoKMB8GA1UdIwQYMBaAFGUMH+hj/pazcLbtUqen4KF4YNxJMDoGA1UdHwQzMDEwL6AtoCuGKWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vY3JsL2lzc3VpbmctY2EuY3JsMHUGCCsGAQUFBwEBBGkwZzAnBggrBgEFBQcwAYYbaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9vY3NwMDwGCCsGAQUFBzAChjBodHRwOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvaXNzdWluZy1jYS5jcnQwTQYDVR0gBEYwRDBCBgorBgEEAYPoXgEBMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vY2EuZW5jeXBoZXIuY29tL3JlcG9zaXRvcnkvY3BzMBkGCSsGAQQBg+heAwQMBgorBgEEAYPoXgMKMDMGCSsGAQQBg+heBAQmDCQwMTlkMjI0MS1lYjk3LTc3MjgtOWVjMC1jZGFhZmJhMzAwYzIwCgYIKoZIzj0EAwMDaQAwZgIxAPXvevd8+FKCy9hXvQRZHG4JYfgny0TY5bpYBP1T8i1a8YG/xWrbRIJITK1iMn0JogIxAPibTB2Pq8Ckvz/aNjG1X0h+bb13E615KT6qWh01Vu4Pyytj3/nTLkW1ZYou9OePYVkD6zCCA+cwggNuoAMCAQICFE99RbNxCOhQOov3zhDPhvHFG0swMAoGCCqGSM49BAMDMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSMwIQYDVQQDDBpFbmN5cGhlciBDMlBBIFJvb3QgQ0EgMjAyNjAeFw0yNjExMDIwMTQxMjlaFw0zMTExMDMwMTQxMjlaMIGRMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSYwJAYDVQQDDB1FbmN5cGhlciBDMlBBIElzc3VpbmcgQ0EgMjAyNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOehmI2TPhJvCzHqbQre/SEVewkylgu9cxvQdh5UC8JulDQ4Eo8waJEr0upe3LJ0b4sQfIbfJ1udXSrgFKD4vEc8HlDAj7U9vCu0zXXjkw1/5HsNmPD0fRq3NXy3OswCuqOCAYYwggGCMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCAGA1UdJQQZMBcGCisGAQQBg+heAgEGCSqGSIb3LwEBBTAdBgNVHQ4EFgQUZQwf6GP+lrNwtu1Sp6fgoXhg3EkwHwYDVR0jBBgwFoAU6G5XZysFzIwLWHhxfLWRdN+5ah4wNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvcm9vdC1jYS5jcmwwcgYIKwYBBQUHAQEEZjBkMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwOQYIKwYBBQUHMAKGLWh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9yb290LWNhLmNydDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMwCgYIKoZIzj0EAwMDZwAwZAIwVyKy+PX6eryaIRifeEi5VgbOghUjEn5AAn2G2wHVq6Bf/lhO3tYfyY3X5xhcW3b8AjAuW4s4UIm2lljp/TeScxKBRCiuj4UTE8cGQJNt2S1rzFmntOP+Uo7OMGKn1DdunJCiZ3NpZ1RzdDKhaXRzdFRva2Vuc4GhY3ZhbFkGozCCBp8GCSqGSIb3DQEHAqCCBpAwggaMAgEDMQ8wDQYJYIZIAWUDBAICBQAwgYwGCyqGSIb3DQEJEAEEoH0EezB5AgEBBgorBgEEAYPoXgUBMDEwDQYJYIZIAWUDBAIBBQAEIMjGvfwNbLSBSG/GfOsKRaAirAL5FIXPYoAje1ZfG0dpAhQ+pQZtc76iKTMuzXCyiQM9icxBRRgPMjAyNjExMDIwMTQxMzBaMAMCAQECCQDQfTfmcrI0eKCCA+wwggPoMIIDbqADAgECAhQoeKpdmqtzWZUbLWXhGy79mL0vizAKBggqhkjOPQQDAzCBlTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFzAVBgNVBAoMDkVuY3lwaGVyIENvcnAuMRQwEgYDVQQLDAtDQSBEaXZpc2lvbjEqMCgGA1UEAwwhRW5jeXBoZXIgQzJQQSBUU0EgSXNzdWluZyBDQSAyMDI2MB4XDTI2MTEwMjAxNDEyOVoXDTI3MTEwMjAxNDEyOVowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xITAfBgNVBAMMGEVuY3lwaGVyIEMyUEEgVFNBIFNpZ25lcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABGv2BhCb52j9boQh4o3ZR/mr2fwGgeZGrANH9pOO/uaeyODk9gGmh6GenNSnqIUFYN7woDI5ONTH3tAPrQIm7YVQu6keogVN3kuvxMPb3TdL+8vc904sziaqEH+TKjGjhqOCAYQwggGAMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgbAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMB0GA1UdDgQWBBQy0HupUYSXYMQ3OaJOEocz1owQrzAfBgNVHSMEGDAWgBQZVh9Y111wrqkyEKYc8ye0t0KUSzBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9jcHMweQYIKwYBBQUHAQEEbTBrMCcGCCsGAQUFBzABhhtodHRwOi8vY2EuZW5jeXBoZXIuY29tL29jc3AwQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS90c2EtaXNzdWluZy1jYS5jcnQwPgYDVR0fBDcwNTAzoDGgL4YtaHR0cDovL2NhLmVuY3lwaGVyLmNvbS9jcmwvdHNhLWlzc3VpbmctY2EuY3JsMAoGCCqGSM49BAMDA2gAMGUCMDGW0eZQ2bre4muVamkRdvMfxTre8s78q6IpmHs7JktYnvehwFkut3QgdLELPdKWfQIxAJiLhLjPA5fQWD3FCHr8MszLQ7qMidnEiKf44YJsPF2G14CivkTwsVCP9VDA2qQbPTGCAfUwggHxAgEBMIGuMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEXMBUGA1UECgwORW5jeXBoZXIgQ29ycC4xFDASBgNVBAsMC0NBIERpdmlzaW9uMSowKAYDVQQDDCFFbmN5cGhlciBDMlBBIFRTQSBJc3N1aW5nIENBIDIwMjYCFCh4ql2aq3NZlRstZeEbLv2YvS+LMA0GCWCGSAFlAwQCAgUAoIG2MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAeBgkqhkiG9w0BCQUxERgPMjAyNjExMDIwMTQxMzBaMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIDxp/HaGi5RL9ideEDYibfB46z1dX4Gi4y7yJIR8TLBSMD8GCSqGSIb3DQEJBDEyBDAh/BmrfXz1XXtOzS7tvicev1x2bMJJh+5WsHqX6nLFlFjAC9N0/tumkP7d08FmpL8wCgYIKoZIzj0EAwMEZzBlAjBRHfd+fp4VJ+yRTU8MA7LDuhkEk79r039Ph0mSXjCNtBmMrhQCjnmByUdABpPjvMgCMQDYSWm/KGsTCltvkKRw5woYXiB6r9G7UxPvInNs2WL1FqFaryVeqxvKQnuCI8j5bw8aQ1BBZFl5QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYYFgei09puVcdqnACS6UgLtATw3h3einJdEdx5nVa9HXNuugdz9Mnsv8VbitG6S1EhTgBd8PdPRCJtm7BEtz5l9hOhEKJingyz9be8Wuss2AySXnMQgdBlSp5JQCO6/pUMg==","manifest_sha256":"5b153eab95513224c7a3028aa49b478e4313b4aabf8069e8b91c100584547b9e","carrier_sha256":"d254e0a642933492db5b05dfb56e2fab848b56d6211962f014ade509f07f738c","expected_final_size":37326,"verification_url":"https://api.async-prepared-fixture.publisher.example.com/api/v1/verify/doc_217fa14b83331131dfdaeb1dd77a88f4","watermark_applied":false,"reused":false,"policy_digest":"171bf3407b6bb613d78ec55255c2a908acf9f591c2e9aa6e96e4f15e67370cff"}} \ No newline at end of file diff --git a/crates/provenance/encypher/src/fixtures/prepared/webp/source.webp b/crates/provenance/encypher/src/fixtures/prepared/webp/source.webp new file mode 100644 index 0000000000..23d0b3d175 Binary files /dev/null and b/crates/provenance/encypher/src/fixtures/prepared/webp/source.webp differ diff --git a/crates/provenance/encypher/src/lib.rs b/crates/provenance/encypher/src/lib.rs new file mode 100644 index 0000000000..622bbf606a --- /dev/null +++ b/crates/provenance/encypher/src/lib.rs @@ -0,0 +1,856 @@ +//! Encypher provenance for article pages and their media, `provenance.encypher`. +//! +//! A serve middleware finds the article in each HTML page a `[[serve]]` entry +//! names it for, sends its visible text to the Encypher signing API, and +//! embeds the returned invisible markers and a manifest meta tag. Plans are +//! cached in the runtime KV store, bound to a short-lived server policy. +//! Every failure passes the page through unchanged. +//! +//! Media is signed as its exact bytes through the Encypher media endpoint, +//! under the same policy and worker token: images, audio, video and fonts +//! core captures on the publisher and asset proxy paths, through the +//! module's media signer; media `data:` URIs in the same pages, and their +//! inline SVG as the standalone SVG document proven to parse to the tree the +//! browser built, which is the original bytes where they already are one; +//! and media `data:` URIs in the stylesheets under each `path` a +//! `[[serve]]` entry names `provenance.encypher.stylesheet` for, which the +//! publisher asserts no page loads with `integrity`. Stylesheets under no +//! such path are left as they are. A media item that cannot be signed is +//! delivered as it was and reported unsigned, never as signed. +//! +//! The middleware's preparation applies the request gates with no I/O: only +//! a `GET` of a `200` page on the configured domain, whose origin did not +//! mark it as one reader's, is held. The signing work runs in the stream +//! processor's asynchronous finish, which core awaits at the end of the +//! document. The module requires no permission and reads no gated value. It +//! sends the visible text of the page's article, the page address without +//! its query, and the bytes of the media it signs. A page whose origin set a +//! cookie or forbade shared caching is never sent, but the module cannot +//! tell a personalized article that carries neither, so a deployment places +//! it only on public article routes and has its origin mark every +//! personalized response private. +//! +//! Independently of signing admission, the applicable page middleware protects +//! original provenance-bearing or structurally uncertain inline SVG ranges. +//! Core holds the bounded original before its own rewrites, keeps those ranges +//! opaque through fetch/serve changes and restores their exact bytes before +//! publishing a template or reader response. The same classifier decides both +//! this protection and the module's descendant-edit refusals. + +use std::any::Any; +use std::sync::Arc; + +use error_stack::Report; +use serde::Deserialize; +use validator::{Validate, ValidationError}; + +use trusted_server_core::error::TrustedServerError; +use trusted_server_core::integrations::{ + IntegrationBuilder, IntegrationRegistration, ModuleSecretSetting, SuppliedMiddleware, +}; +use trusted_server_core::middleware::{ + HTML_MEDIA_TYPE, Middleware, MiddlewareAction, MiddlewareContext, MiddlewarePhase, Preparation, +}; +use trusted_server_core::module_context::{FromModuleContext as _, ModuleCall, ModuleRequest}; +use trusted_server_core::redacted::Redacted; +use trusted_server_core::settings::{IntegrationConfig, Settings}; + +mod api; +mod boundary; +mod data_uri; +mod embed; +mod embedded; +mod media_api; +pub mod preparation; +mod prepared_probe; +mod processor; +mod svg_equivalence; +mod text; + +use processor::{ + EncypherMedia, PageRequest, Signer, document_processor, is_excluded_path, is_excluded_prefix, + stylesheet_processor, +}; + +/// The id the module's registration and its API backend carry. +const INTEGRATION_ID: &str = "provenance_encypher"; + +/// The name `[provenance] module` selects the module by and a `[[serve]]` +/// entry names its middleware by, from this crate's folder. +pub const MODULE: &str = "provenance.encypher"; + +/// The name a `[[serve]]` entry for stylesheets writes, which signs the media +/// `data:` URIs a stylesheet embeds. +pub const STYLESHEET_MIDDLEWARE: &str = "provenance.encypher.stylesheet"; + +/// The media type [`STYLESHEET_MIDDLEWARE`] works on. +const STYLESHEET_MEDIA_TYPE: &str = "text/css"; + +/// `[provenance.encypher]`, the module's settings. +#[derive(Debug, Clone, Deserialize, Validate)] +#[serde(deny_unknown_fields)] +#[validate(schema(function = "validate_hold_exceeds_timeout"))] +#[validate(schema(function = "validate_execution"))] +pub struct EncypherConfig { + /// Encypher API origin, for example `https://api.example.com`. Must be https. + #[validate(custom(function = validate_api_base_url))] + pub api_base_url: String, + /// Claimed publisher domain: a bare lowercase hostname without `www.`. + #[validate(custom(function = validate_domain))] + pub domain: String, + /// The name of the secret-store entry holding the `cwk_` worker + /// credential, resolved to the credential itself when settings load. + pub worker_token: Redacted, + /// Optional article selector override: one unescaped `.class`, an exact + /// `[attr=value]` (also single- or double-quoted), or an ASCII tag name. + /// Empty means no override; a valid selector absent from a page keeps + /// the detection fallback. At most 91 characters, so + /// `override:{selector}` fits the API's 100-character limit. + #[serde(default)] + #[validate(length(max = 91), custom(function = validate_article_selector))] + pub article_selector: String, + /// Minimum visible article text, in Unicode scalars. + #[serde(default = "default_min_text_length")] + #[validate(range(min = 50, max = 50_000))] + pub min_text_length: usize, + /// Largest decoded document held for signing. Capped at + /// `publisher.max_buffered_body_bytes`. + #[serde(default = "default_max_document_bytes")] + #[validate(range(min = 1024))] + pub max_document_bytes: usize, + /// First-byte timeout for each policy or sign call, in milliseconds. The + /// only bound on a hung API on a runtime without a timer. + #[serde(default = "default_timeout_ms")] + #[validate(range(min = 250, max = 10_000))] + pub timeout_ms: u64, + /// Longest one document is held for signing, in milliseconds, on a + /// runtime with a timer. Must exceed `timeout_ms`. + #[serde(default = "default_max_hold_ms")] + #[validate(range(min = 500, max = 15_000))] + pub max_hold_ms: u64, + /// Lifetime of a cached signing plan, in seconds. + #[serde(default = "default_plan_ttl_seconds")] + #[validate(range(min = 60, max = 31_536_000))] + pub plan_ttl_seconds: u64, + /// Lifetime of the server-authoritative policy, in seconds. + #[serde(default = "default_policy_ttl_seconds")] + #[validate(range(min = 60, max = 300))] + pub policy_ttl_seconds: u64, + /// Lifetime of a cached signing failure, in seconds. + #[serde(default = "default_negative_ttl_seconds")] + #[validate(range(min = 60, max = 86_400))] + pub negative_ttl_seconds: u64, + /// Largest media item signed, in bytes: a captured image or a decoded + /// embedded item. Larger media is delivered unsigned. Bounded by what the + /// endpoint embeds as source data when the source has no safe address. + #[serde(default = "default_max_media_bytes")] + #[validate(range(min = 1024, max = 8_000_000))] + pub max_media_bytes: usize, + /// Maximum uncached media-signing calls in one HTML or stylesheet pass. + /// Cached assets do not consume calls. The document's existing time and + /// output bounds still apply independently. + #[serde(default = "default_max_media_sign_calls_per_document")] + #[validate(range(min = 1, max = 128))] + pub max_media_sign_calls_per_document: usize, + /// Largest media signed through the prepared lane, in bytes: hashed in + /// bounded chunks where it is delivered, never uploaded or held whole, + /// and given a remote manifest its bytes are left unchanged by. `0`, the + /// default, keeps the lane off, so no deployment starts hashing large + /// files on its own; turning it on also needs the signed probe release + /// and the prepared endpoints. Otherwise at least 16 MiB, and at most the + /// prepared service's largest source. Media between `max_media_bytes` + /// and this is the lane's. A bounded example is `1073741824` (1 GiB) + /// with `max_large_media_ms = 60000`. + #[serde(default = "default_max_large_media_bytes")] + #[validate(custom(function = validate_max_large_media_bytes))] + pub max_large_media_bytes: u64, + /// Longest one large source may take, in milliseconds: core's hash of + /// the whole source and the module's probe and API calls together. A + /// source not signed in time is delivered unsigned. + #[serde(default = "default_max_large_media_ms")] + #[validate(range(min = 5_000, max = 600_000))] + pub max_large_media_ms: u64, + /// How the module signs: `synchronous`, the default, where a reader's + /// delivery signs what it holds no result for, or `async_prepared`, + /// where the publisher's preparation worker signs and delivery only + /// looks up published results, see [`preparation`]. + #[serde(default)] + pub execution: preparation::Execution, + /// `[provenance.encypher.preparation]`, required by `async_prepared`. + #[serde(default)] + #[validate(nested)] + pub preparation: Option, +} + +/// The prepared service's largest source, in bytes: the largest integer a +/// JSON number carries exactly, less the room it keeps for a manifest. +const MAX_PREPARED_SOURCE_BYTES: u64 = 9_007_199_246_352_383; +/// The smallest non-zero `max_large_media_bytes`, above every captured size. +const MIN_LARGE_MEDIA_BYTES: u64 = 16 * 1024 * 1024; + +impl IntegrationConfig for EncypherConfig {} + +fn default_min_text_length() -> usize { + 50 +} + +fn default_max_document_bytes() -> usize { + 5_000_000 +} + +fn default_timeout_ms() -> u64 { + 3_000 +} + +fn default_max_hold_ms() -> u64 { + 5_000 +} + +fn default_plan_ttl_seconds() -> u64 { + 604_800 +} + +fn default_policy_ttl_seconds() -> u64 { + 300 +} + +fn default_negative_ttl_seconds() -> u64 { + 300 +} + +fn default_max_media_bytes() -> usize { + 5_000_000 +} + +fn default_max_media_sign_calls_per_document() -> usize { + 8 +} + +fn default_max_large_media_bytes() -> u64 { + 0 +} + +fn default_max_large_media_ms() -> u64 { + 60_000 +} + +fn validate_max_large_media_bytes(value: u64) -> Result<(), ValidationError> { + if value == 0 || (MIN_LARGE_MEDIA_BYTES..=MAX_PREPARED_SOURCE_BYTES).contains(&value) { + Ok(()) + } else { + Err(ValidationError::new("encypher_max_large_media_bytes")) + } +} + +fn validate_article_selector(value: &str) -> Result<(), ValidationError> { + if value.is_empty() || boundary::is_valid_selector(value) { + Ok(()) + } else { + Err(ValidationError::new("encypher_article_selector")) + } +} + +fn validate_api_base_url(value: &str) -> Result<(), ValidationError> { + let parsed = + url::Url::parse(value).map_err(|_| ValidationError::new("encypher_api_base_url"))?; + if parsed.scheme() != "https" + || parsed.host_str().is_none_or(str::is_empty) + || parsed.query().is_some() + || parsed.fragment().is_some() + { + return Err(ValidationError::new("encypher_api_base_url")); + } + Ok(()) +} + +fn validate_domain(value: &str) -> Result<(), ValidationError> { + let valid_label = |label: &str| { + !label.is_empty() + && label.len() <= 63 + && !label.starts_with('-') + && !label.ends_with('-') + && label + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') + }; + let valid = value.len() <= 253 + && value.contains('.') + && !value.starts_with("www.") + && value.split('.').all(valid_label); + if valid { + Ok(()) + } else { + Err(ValidationError::new("encypher_domain")) + } +} + +fn validate_hold_exceeds_timeout(config: &EncypherConfig) -> Result<(), ValidationError> { + if config.max_hold_ms > config.timeout_ms { + Ok(()) + } else { + Err(ValidationError::new("encypher_max_hold_ms")) + } +} + +/// `async_prepared` needs its preparation table: it never falls back to +/// signing in delivery, so without a worker nothing would ever be signed. +fn validate_execution(config: &EncypherConfig) -> Result<(), ValidationError> { + if config.execution == preparation::Execution::AsyncPrepared && config.preparation.is_none() { + return Err(ValidationError::new( + "async_prepared_requires_preparation_table", + )); + } + Ok(()) +} + +/// The worker token, named in `[provenance.encypher]` by its entry in the +/// secret store, which is in use whenever the module is selected, and the +/// preparation scopes' secrets, in use in `async_prepared` mode. +const SECRET_SETTINGS: &[ModuleSecretSetting] = &[ + ModuleSecretSetting { + path: &["worker_token"], + in_use: always_in_use, + }, + ModuleSecretSetting { + path: &["preparation", "signal_secret"], + in_use: async_prepared_in_use, + }, + ModuleSecretSetting { + path: &["preparation", "publication_secret"], + in_use: async_prepared_in_use, + }, +]; + +fn always_in_use(_table: &serde_json::Map) -> bool { + true +} + +fn async_prepared_in_use(table: &serde_json::Map) -> bool { + table.get("execution").and_then(serde_json::Value::as_str) == Some("async_prepared") + && table.contains_key("preparation") +} + +/// The changes the module supplies, as `[[serve]]` entries name them: the +/// page's article and embedded media, and a stylesheet's embedded media. +const SUPPLIED_MIDDLEWARE: &[SuppliedMiddleware] = &[ + SuppliedMiddleware::on_html(MODULE, MiddlewarePhase::Serve), + SuppliedMiddleware::on( + STYLESHEET_MIDDLEWARE, + MiddlewarePhase::Serve, + STYLESHEET_MEDIA_TYPE, + ), +]; + +/// The builder a deployment installs to offer this module. +#[must_use] +pub fn builder() -> IntegrationBuilder { + IntegrationBuilder::new(INTEGRATION_ID, env!("CARGO_PKG_NAME"), register, validate) + .with_module_name(MODULE) + .with_cache_features(&[trusted_server_core::media_signing::ASSETS_CACHE]) + .with_secret_settings(SECRET_SETTINGS) + .with_middleware(SUPPLIED_MIDDLEWARE) + // Removes every preparation snapshot capability from every request, + // and admits only one that verifies, see `preparation::edge`. + .with_request_preparer(preparation::edge::prepare_request) +} + +/// Refuses an `async_prepared` table whose public host is not the claimed +/// domain or its `www.` name, so every prepared address is on the claim. +fn require_claimed_public_host(config: &EncypherConfig) -> Result<(), Report> { + let Some(preparation) = config + .preparation + .as_ref() + .filter(|_| config.execution == preparation::Execution::AsyncPrepared) + else { + return Ok(()); + }; + let host = preparation.public_host.as_str(); + if host.strip_prefix("www.").unwrap_or(host) == config.domain { + return Ok(()); + } + Err(Report::new(TrustedServerError::Settings { + message: format!( + "[provenance.encypher.preparation] public_host `{host}` is not the claimed domain \ + `{}` or its `www.` name", + config.domain + ), + })) +} + +/// Whether `[asset-cache]` selects a module, which then holds reader copies +/// keyed by the host and scheme the reader asked. +fn selects_asset_cache(settings: &Settings) -> bool { + settings + .sections + .section(trusted_server_core::asset_cache::MODULE_TYPE) + .is_some_and(|modules| !modules.selected().is_empty()) +} + +/// With an asset cache selected, requires an `async_prepared` table's +/// `loopback_url` to be `https://`, so a snapshot reads the +/// cached copy a reader would get, under the host and scheme the cache keys +/// it by. A trailing `/` and the default port `:443` are the only other +/// spellings admitted, and the registered origin drops both, so a snapshot +/// addresses exactly what a reader does. Synchronous mode, and an +/// `async_prepared` deployment with no asset cache, keep the general origin +/// rules, including local `http` on a loopback host. +fn require_public_snapshot_origin( + settings: &Settings, + config: &mut EncypherConfig, +) -> Result<(), Report> { + if config.execution != preparation::Execution::AsyncPrepared || !selects_asset_cache(settings) { + return Ok(()); + } + let Some(preparation) = config.preparation.as_mut() else { + return Ok(()); + }; + let written = preparation.loopback_url.as_str(); + let origin = written.strip_suffix('/').unwrap_or(written); + let authority = origin.strip_prefix("https://"); + if authority.is_some_and(|authority| { + authority == preparation.public_host + || authority.strip_suffix(":443") == Some(preparation.public_host.as_str()) + }) { + preparation + .loopback_url + .truncate("https://".len() + preparation.public_host.len()); + return Ok(()); + } + Err(Report::new(TrustedServerError::Settings { + message: format!( + "[provenance.encypher.preparation] loopback_url must be `https://{}` while \ + [asset-cache] selects a module: snapshots read through it, and the asset cache \ + keys each held copy by the host and scheme a reader asked, so any other origin \ + would prepare content readers are not served", + preparation.public_host + ), + })) +} + +/// What the platform the module registers on can do for a reader's hint, +/// as its adapter declared before serving. +#[derive(Debug, Clone, Copy)] +struct HintLifetimes { + /// Every request's services carry a real timer. + timer: bool, + /// The instance sends handed-over requests after the response. + after_response: bool, +} + +impl HintLifetimes { + fn declared() -> Self { + Self { + timer: trusted_server_core::platform::platform_offers_timer(), + after_response: trusted_server_core::platform::platform_offers_after_response_handoff(), + } + } +} + +/// Refuses reader hints on a platform that can neither bound a hint while +/// the reader waits nor send it after the response. Warming, publication +/// events and reconciliation still prepare content there. +fn require_reader_trigger_capability( + config: &EncypherConfig, + lifetimes: HintLifetimes, +) -> Result<(), Report> { + let wants = config.execution == preparation::Execution::AsyncPrepared + && config + .preparation + .as_ref() + .is_some_and(|preparation| preparation.reader_trigger); + if !wants || lifetimes.timer || lifetimes.after_response { + return Ok(()); + } + Err(Report::new(TrustedServerError::Settings { + message: "[provenance.encypher.preparation] reader_trigger needs a platform with a \ + timer or an after-response lifetime, and this one declares neither; set \ + reader_trigger = false and prepare through warming, publication events and \ + reconciliation" + .to_owned(), + })) +} + +/// Refuses a document that selects the module and names its middleware in no +/// `[[serve]]` entry, where no page would ever be signed. +fn require_an_entry(settings: &Settings) -> Result<(), Report> { + let placed = settings + .serve + .entries() + .iter() + .any(|entry| entry.middleware.iter().any(|name| name == MODULE)); + if placed { + return Ok(()); + } + Err(Report::new(TrustedServerError::Settings { + message: format!( + "[provenance] selects `encypher`, but no [[serve]] entry names `{MODULE}`, so no \ + page would be signed. Add `{MODULE}` to the [[serve]] entry that covers the \ + article pages, with a `path` for each section to sign" + ), + })) +} + +/// Refuses a `[[serve]]` entry that names the stylesheet middleware without +/// a `path` narrower than the whole site. +/// +/// Signing a stylesheet's media changes its bytes, which breaks a page that +/// loads it with a subresource integrity value, and the middleware cannot +/// see the page that loads it. So it runs only on stylesheets the publisher +/// names by path, asserting none of them is loaded with `integrity`; a bare +/// `text/css` entry, or one for `/`, would rewrite every stylesheet. +fn require_scoped_stylesheet_entries( + settings: &Settings, +) -> Result<(), Report> { + let unscoped = settings.serve.entries().iter().any(|entry| { + entry + .middleware + .iter() + .any(|name| name == STYLESHEET_MIDDLEWARE) + && entry + .path + .as_deref() + .is_none_or(|path| path.is_empty() || path == "/") + }); + if !unscoped { + return Ok(()); + } + Err(Report::new(TrustedServerError::Settings { + message: format!( + "a [[serve]] entry names `{STYLESHEET_MIDDLEWARE}` for every path. Signing a \ + stylesheet's embedded media changes its bytes and breaks a page that loads it \ + with `integrity`, so give the entry a `path` for the stylesheets no page loads \ + with `integrity`" + ), + })) +} + +/// The module's settings when a section selects it. +fn config(settings: &Settings) -> Result, Report> { + let Some(mut config) = settings.module_config::(MODULE)? else { + return Ok(None); + }; + require_an_entry(settings)?; + require_scoped_stylesheet_entries(settings)?; + require_claimed_public_host(&config)?; + require_public_snapshot_origin(settings, &mut config)?; + Ok(Some(config)) +} + +/// Validates the module's table and reports whether a section selects the +/// module. +/// +/// # Errors +/// +/// Returns an error when the table cannot be read or fails its validation, or +/// when no `[[serve]]` entry names the middleware. +pub fn validate(settings: &Settings) -> Result> { + Ok(config(settings)?.is_some()) +} + +/// Registers the middleware and the media signer when a section selects the +/// module and its worker token is not blank. +/// +/// # Errors +/// +/// Returns the errors [`validate`] returns. +pub fn register( + settings: &Settings, +) -> Result, Report> { + let Some(config) = config(settings)? else { + return Ok(None); + }; + if config.worker_token.expose().trim().is_empty() { + log::warn!("encypher: not registered, because the worker token is empty"); + return Ok(None); + } + require_reader_trigger_capability(&config, HintLifetimes::declared())?; + log::info!( + "encypher: registered for domain={} execution={:?}", + config.domain, + config.execution + ); + let asynchronous = config.execution == preparation::Execution::AsyncPrepared; + let max_document_bytes = config + .max_document_bytes + .min(settings.publisher.max_buffered_body_bytes); + let signer = Arc::new(Signer::new(config, max_document_bytes)); + let mut registration = IntegrationRegistration::builder(INTEGRATION_ID) + .without_js() + .with_middleware(Arc::new(Encypher { + signer: Arc::clone(&signer), + })) + .with_middleware(Arc::new(EncypherStylesheet { + signer: Arc::clone(&signer), + })) + .with_media_signer(Arc::new(EncypherMedia::new(Arc::clone(&signer)))); + if asynchronous { + registration = + registration.with_proxy(Arc::new(preparation::edge::PreparationRoutes::new(signer))); + } + Ok(Some(registration.build())) +} + +/// The middleware, which signs the article and the embedded media of each +/// admitted page. +pub struct Encypher { + signer: Arc, +} + +impl std::fmt::Debug for Encypher { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.debug_struct("Encypher").finish_non_exhaustive() + } +} + +/// What the preparation decided for one document. +enum Admission { + /// Nothing to do. + Pass, + /// The page to sign or look up. + Hold(PageRequest), + /// A verified preparation snapshot read: nothing is signed or looked + /// up, and the answer reports what the origin's own headers said. + Snapshot(preparation::edge::SnapshotGrant), +} + +/// The snapshot grant the module's request preparer left on this +/// document's request, in the asynchronous mode. +fn snapshot_grant( + signer: &Signer, + context: &MiddlewareContext<'_>, +) -> Option { + if !signer.is_async() { + return None; + } + context + .document_state + .get::(INTEGRATION_ID) + .map(|grant| (*grant).clone()) +} + +/// The facts header a snapshot read of a document answers with: the +/// origin's status and privacy as core read them before its own rewrites. +fn snapshot_action( + signer: &Signer, + context: &MiddlewareContext<'_>, + grant: &preparation::edge::SnapshotGrant, +) -> MiddlewareAction { + let Some(preparation) = signer.preparation() else { + return MiddlewareAction::pass(); + }; + let headers = context.response_headers; + let directive = |name: &str| { + headers.is_some_and(|headers| { + headers + .get_all(http::header::CACHE_CONTROL) + .iter() + .filter_map(|value| value.to_str().ok()) + .flat_map(|value| value.split(',')) + .any(|part| part.trim().eq_ignore_ascii_case(name)) + }) + }; + let manifest_link = headers.is_some_and(|headers| { + headers + .get_all(http::header::LINK) + .iter() + .filter_map(|value| value.to_str().ok()) + .any(|value| value.to_ascii_lowercase().contains("c2pa-manifest")) + }); + let origin = preparation::protocol::SnapshotOrigin { + status: context.status.as_u16(), + private: context.origin_private, + no_transform: directive("no-transform"), + manifest_link, + content_encoding: String::new(), + origin_host: context.origin_host.to_ascii_lowercase(), + // HTML identity binds the actual bounded projection, and this + // context does not expose core's configured media-origin identity. + // An empty digest cannot admit a large-source locator. + origin_identity_sha256: String::new(), + source_reference: None, + }; + // The facts above were read from the origin's own headers; the + // snapshot's transport is then marked internal so no cache keeps it + // for a reader. + let transport = ( + http::header::CACHE_CONTROL, + http::HeaderValue::from_static("private, no-store"), + ); + MiddlewareAction { + response_headers: preparation::edge::snapshot_facts(preparation, grant, &origin) + .into_iter() + .chain(std::iter::once(transport)) + .collect(), + ..MiddlewareAction::pass() + } +} + +impl Middleware for Encypher { + fn middleware_id(&self) -> &'static str { + MODULE + } + + fn finalizes_document(&self) -> bool { + true + } + + fn handles_media_type(&self, media_type: &str) -> bool { + media_type == HTML_MEDIA_TYPE + } + + fn source_protection(&self) -> Option { + Some(embedded::source_protection) + } + + /// Every path but an API, feed, admin or well-known path, or an asset, + /// which is never an article. + fn handles_path(&self, path: &str) -> bool { + !is_excluded_path(path) + } + + /// Serve only: the signed page is written on each reader's copy, so a + /// failed signing is never stored for every reader. + fn phases(&self) -> &[MiddlewarePhase] { + &[MiddlewarePhase::Serve] + } + + /// Applies the request gates, with no I/O, and keeps the page to sign. + fn prepare<'a>( + &'a self, + call: ModuleCall<'a>, + context: &'a MiddlewareContext<'a>, + ) -> Option> { + let admission = match snapshot_grant(&self.signer, context) { + Some(grant) => Admission::Snapshot(grant), + None => admit(&call, context, |page| self.signer.admit(page)), + }; + Some(Box::pin(async move { + Ok(Box::new(admission) as Box) + })) + } + + /// Holds the admitted page, with the security policies its response + /// carries, which decide whether inline style media may be edited. + fn create(&self, context: &MiddlewareContext<'_>) -> MiddlewareAction { + let page = match context.take_prepared::(MODULE) { + Some(Admission::Hold(page)) => page, + Some(Admission::Snapshot(grant)) => { + return snapshot_action(&self.signer, context, &grant); + } + Some(Admission::Pass) | None => return MiddlewareAction::pass(), + }; + let policies = context + .response_headers + .and_then(embedded::content_security_policies); + MiddlewareAction { + stream: Some(Box::new(document_processor(&self.signer, page, policies))), + ..MiddlewareAction::pass() + } + } +} + +/// The stylesheet middleware, which signs the media `data:` URIs of each +/// admitted stylesheet. +pub struct EncypherStylesheet { + signer: Arc, +} + +impl std::fmt::Debug for EncypherStylesheet { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("EncypherStylesheet") + .finish_non_exhaustive() + } +} + +impl Middleware for EncypherStylesheet { + fn middleware_id(&self) -> &'static str { + STYLESHEET_MIDDLEWARE + } + + fn finalizes_document(&self) -> bool { + true + } + + fn handles_media_type(&self, media_type: &str) -> bool { + media_type == STYLESHEET_MEDIA_TYPE + } + + /// Every path but an API, feed, admin or well-known path. A `.css` + /// suffix is what a stylesheet has. + fn handles_path(&self, path: &str) -> bool { + !is_excluded_prefix(path) + } + + /// Serve only, as for pages. + fn phases(&self) -> &[MiddlewarePhase] { + &[MiddlewarePhase::Serve] + } + + /// Applies the request gates, with no I/O, and keeps the stylesheet to + /// sign. + fn prepare<'a>( + &'a self, + call: ModuleCall<'a>, + context: &'a MiddlewareContext<'a>, + ) -> Option> { + let admission = match snapshot_grant(&self.signer, context) { + Some(grant) => Admission::Snapshot(grant), + None => admit(&call, context, |page| self.signer.admit_stylesheet(page)), + }; + Some(Box::pin(async move { + Ok(Box::new(admission) as Box) + })) + } + + fn create(&self, context: &MiddlewareContext<'_>) -> MiddlewareAction { + let page = match context.take_prepared::(STYLESHEET_MIDDLEWARE) { + Some(Admission::Hold(page)) => page, + Some(Admission::Snapshot(grant)) => { + return snapshot_action(&self.signer, context, &grant); + } + Some(Admission::Pass) | None => return MiddlewareAction::pass(), + }; + MiddlewareAction { + stream: Some(Box::new(stylesheet_processor(&self.signer, page))), + ..MiddlewareAction::pass() + } + } +} + +/// The document a request is for, when `gate` admits it. +fn admit( + call: &ModuleCall<'_>, + context: &MiddlewareContext<'_>, + gate: impl FnOnce(PageRequest) -> Result, +) -> Admission { + let Ok(request) = ModuleRequest::from_module_context(call) else { + return Admission::Pass; + }; + let removed_prefix = request.removed_prefix().unwrap_or_default(); + let page = PageRequest { + method: request.method().clone(), + // The address the reader asked for: a host that serves several + // publishers by path signs each page for its own address. + page_url: format!( + "{}://{}{removed_prefix}{}", + request.scheme(), + request.host(), + request.path() + ), + query: request.query().to_owned(), + removed_prefix: removed_prefix.to_owned(), + response_status: context.status.as_u16(), + origin_response_is_private: context.origin_private, + }; + match gate(page) { + Ok(page) => Admission::Hold(page), + Err(gate) => { + log::debug!("encypher: document not held, gate={}", gate.code()); + Admission::Pass + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/provenance/encypher/src/media_api.rs b/crates/provenance/encypher/src/media_api.rs new file mode 100644 index 0000000000..3f35977529 --- /dev/null +++ b/crates/provenance/encypher/src/media_api.rs @@ -0,0 +1,832 @@ +//! Wire contract of the Encypher media signing endpoint, +//! `POST /api/v1/public/cdn/media/sign`. +//! +//! The request is `multipart/form-data` with a `metadata` JSON part and the +//! exact `source` bytes. Neither core nor this module transforms media, so no +//! `final` part is sent and the execution record lists no operations. A `200` +//! answer is the exact signed bytes, described by `X-Encypher-*` headers that +//! [`verify_signed_response`] checks before any byte reaches a reader. + +use bytes::{BufMut as _, Bytes, BytesMut}; +use http::{HeaderMap, HeaderValue}; +use serde::ser::SerializeSeq as _; +use serde::{Deserialize, Serialize, Serializer}; +use sha2::{Digest as _, Sha256}; +use trusted_server_core::middleware::media_type_of; + +use crate::api::valid_policy_digest; + +/// The media signing route, below the API origin. +pub(crate) const MEDIA_SIGN_PATH: &str = "/api/v1/public/cdn/media/sign"; +/// The request header carrying the replay key the endpoint requires. +pub(crate) const IDEMPOTENCY_KEY_HEADER: &str = "idempotency-key"; +/// The whole multipart body the endpoint accepts. +pub(crate) const MAX_REQUEST_BYTES: usize = 100_000_000; +/// What signing may add to the bytes: the manifest store with its +/// certificate chain and the embedded source data. +pub(crate) const MAX_MANIFEST_OVERHEAD_BYTES: usize = 10 * 1024 * 1024; +/// A failure body is only read for its code. +pub(crate) const MAX_ERROR_BODY_BYTES: usize = 4096; +/// Longest manifest instance or document identifier accepted. +const MAX_IDENTIFIER_BYTES: usize = 256; +/// Longest URL the endpoint accepts or returns. +const MAX_URL_BYTES: usize = 2048; +/// Longest failure code kept for the log. +const MAX_ERROR_CODE_BYTES: usize = 64; + +const POLICY_DIGEST_HEADER: &str = "x-encypher-policy-digest"; +const SIGNED_SHA256_HEADER: &str = "x-encypher-signed-sha256"; +const INSTANCE_ID_HEADER: &str = "x-encypher-manifest-instance-id"; +const DISPOSITION_HEADER: &str = "x-encypher-manifest-disposition"; +const MANIFEST_URL_HEADER: &str = "x-encypher-manifest-url"; +const MANIFEST_SHA256_HEADER: &str = "x-encypher-manifest-sha256"; +const LINK_REQUIRED_HEADER: &str = "x-encypher-manifest-link-required"; +const VERIFICATION_URL_HEADER: &str = "x-encypher-verification-url"; +const DOCUMENT_ID_HEADER: &str = "x-encypher-document-id"; + +/// Every signing header the endpoint answers with, in the order a cached +/// answer stores them. +pub(crate) const SIGNING_HEADERS: [&str; 10] = [ + "content-type", + POLICY_DIGEST_HEADER, + SIGNED_SHA256_HEADER, + INSTANCE_ID_HEADER, + DISPOSITION_HEADER, + MANIFEST_URL_HEADER, + MANIFEST_SHA256_HEADER, + LINK_REQUIRED_HEADER, + VERIFICATION_URL_HEADER, + DOCUMENT_ID_HEADER, +]; + +/// The `metadata` part of one media signing request. +#[derive(Debug, Serialize)] +pub(crate) struct MediaMetadata<'a> { + /// The reader-facing absolute URL the media is delivered on or in. + pub(crate) page_url: &'a str, + /// The server policy the edge holds, which the endpoint refuses when + /// stale. + pub(crate) expected_policy_digest: &'a str, + pub(crate) source: SourceMetadata<'a>, + pub(crate) execution_record: ExecutionRecord<'a>, + pub(crate) delivery: Delivery, +} + +/// What the edge says about the captured source bytes. +#[derive(Debug, Serialize)] +pub(crate) struct SourceMetadata<'a> { + /// A canonical `https` address of the source, or `null` where there is + /// none: embedded media, or an address with a query or credentials, + /// which is never stripped to make one. Without it the endpoint binds the + /// source by embedding its bytes. + pub(crate) reference: Option<&'a str>, + /// The exact hostname the source was fetched from, which the endpoint + /// holds the reference and the claim's managed origins against, or + /// `null` only for media embedded in a document, which was fetched from + /// no host of its own. Never a scheme, port, path or query. + pub(crate) origin_host: Option<&'a str>, + /// The declared media type, a hint the endpoint checks against the bytes. + pub(crate) mime: &'a str, + pub(crate) sha256: &'a str, + pub(crate) byte_length: usize, + /// The source is a platform image optimizer's answer: the endpoint + /// refuses it unless the bytes carry an embedded manifest, which it + /// validates as it validates any ingredient's. Sent only when true. + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub(crate) require_embedded_manifest: bool, +} + +/// What the edge did to the source before it was delivered: nothing. +#[derive(Debug, Serialize)] +pub(crate) struct ExecutionRecord<'a> { + pub(crate) source_sha256: &'a str, + pub(crate) final_sha256: &'a str, + /// The exact source was captured and no operation was applied. + pub(crate) complete: bool, + pub(crate) operations: NoOperations, +} + +/// The empty operation list of media delivered as it was captured. +#[derive(Debug)] +pub(crate) struct NoOperations; + +impl Serialize for NoOperations { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_seq(Some(0))?.end() + } +} + +/// How the signed representation reaches readers. +#[derive(Debug, Serialize)] +pub(crate) struct Delivery { + /// Whether every response of the representation can carry a `Link` + /// header naming a remote manifest. + pub(crate) http_link_header: bool, +} + +impl<'a> MediaMetadata<'a> { + /// Metadata for `source` delivered unmodified: no `final` part, an empty + /// and complete execution record. + pub(crate) fn unmodified( + page_url: &'a str, + expected_policy_digest: &'a str, + source: SourceMetadata<'a>, + http_link_header: bool, + ) -> Self { + let sha256 = source.sha256; + Self { + page_url, + expected_policy_digest, + source, + execution_record: ExecutionRecord { + source_sha256: sha256, + final_sha256: sha256, + complete: true, + operations: NoOperations, + }, + delivery: Delivery { http_link_header }, + } + } +} + +/// A built multipart request body and its `Content-Type`. +#[derive(Debug)] +pub(crate) struct Multipart { + pub(crate) content_type: String, + pub(crate) body: Bytes, +} + +/// Builds the `multipart/form-data` body with the `metadata` and `source` +/// parts, with a boundary that occurs in neither. +pub(crate) fn multipart(metadata: &[u8], source: &[u8]) -> Multipart { + let seed = sha256_hex(metadata); + let mut attempt = 0_u32; + let boundary = loop { + let candidate = format!( + "encypher-media-{}-{attempt}", + seed.get(..32).unwrap_or(&seed) + ); + let delimiter = format!("--{candidate}"); + if !contains(metadata, delimiter.as_bytes()) && !contains(source, delimiter.as_bytes()) { + break candidate; + } + attempt = attempt.saturating_add(1); + }; + let metadata_head = format!( + "--{boundary}\r\nContent-Disposition: form-data; name=\"metadata\"\r\n\ + Content-Type: application/json\r\n\r\n" + ); + let source_head = format!( + "\r\n--{boundary}\r\nContent-Disposition: form-data; name=\"source\"; \ + filename=\"source\"\r\nContent-Type: application/octet-stream\r\n\r\n" + ); + let closing = format!("\r\n--{boundary}--\r\n"); + let mut body = BytesMut::with_capacity( + metadata_head.len() + metadata.len() + source_head.len() + source.len() + closing.len(), + ); + body.put_slice(metadata_head.as_bytes()); + body.put_slice(metadata); + body.put_slice(source_head.as_bytes()); + body.put_slice(source); + body.put_slice(closing.as_bytes()); + Multipart { + content_type: format!("multipart/form-data; boundary={boundary}"), + body: body.freeze(), + } +} + +fn contains(haystack: &[u8], needle: &[u8]) -> bool { + haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +/// Lowercase hex SHA-256 of `bytes`. +pub(crate) fn sha256_hex(bytes: &[u8]) -> String { + hex_lower(&Sha256::digest(bytes)) +} + +/// Lowercase hexadecimal encoding of `bytes`, without hashing them. +pub(crate) fn hex_lower(bytes: &[u8]) -> String { + const DIGITS: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(char::from(DIGITS[usize::from(byte >> 4)])); + out.push(char::from(DIGITS[usize::from(byte & 0x0f)])); + } + out +} + +/// The bare media type an answer must carry for media declared as `declared`, +/// with the common `image/jpg` spelling read as `image/jpeg`, and the +/// `audio/x-aac` and `audio/aacp` spellings of raw AAC as `audio/aac`. +pub(crate) fn canonical_media_type(declared: &str) -> String { + let media_type = media_type_of(declared); + match media_type.as_str() { + "image/jpg" => "image/jpeg".to_owned(), + "audio/x-aac" | "audio/aacp" => "audio/aac".to_owned(), + _ => media_type, + } +} + +/// Whether `reference` may be sent as the source's address: canonical +/// `https` on the default port, with a path, no credentials, query or +/// fragment, on `host`. Anything else is sent as no reference. +pub(crate) fn safe_source_reference(reference: &str, host: &str) -> bool { + if reference.len() > MAX_URL_BYTES || !reference.is_ascii() { + return false; + } + let Ok(parsed) = url::Url::parse(reference) else { + return false; + }; + parsed.scheme() == "https" + && parsed.as_str() == reference + && parsed.username().is_empty() + && parsed.password().is_none() + && parsed.port().is_none() + && parsed.query().is_none() + && parsed.fragment().is_none() + && parsed.path() != "/" + && parsed.host_str() == Some(host) +} + +/// Whether `host` is one canonical hostname alone: no scheme, user +/// information, port, path or query, in lower case. +pub(crate) fn valid_origin_host(host: &str) -> bool { + !host.is_empty() + && host.len() <= 253 + && url::Url::parse(&format!("https://{host}/")).is_ok_and(|parsed| { + parsed.host_str() == Some(host) + && parsed.port().is_none() + && parsed.username().is_empty() + && parsed.path() == "/" + }) +} + +/// Where the signed bytes' manifest lives. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum Disposition { + /// Inside the signed bytes. + Embedded, + /// At [`SignedMedia::manifest_url`], named by the bytes or a `Link`. + External, +} + +/// What a signing answer must match. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Expected<'a> { + pub(crate) policy_digest: &'a str, + /// The bare media type the source was delivered as. + pub(crate) media_type: &'a str, + /// SHA-256 of the bytes as they were before signing. + pub(crate) unsigned_sha256: &'a str, + /// Whether every response of the representation can carry a `Link`. + pub(crate) link_deliverable: bool, +} + +/// A verified signing answer. +#[derive(Debug, Clone)] +pub(crate) struct SignedMedia { + pub(crate) bytes: Bytes, + pub(crate) content_type: HeaderValue, + pub(crate) instance_id: String, + pub(crate) document_id: String, + pub(crate) verification_url: String, + pub(crate) disposition: Disposition, + pub(crate) manifest_url: Option, +} + +impl SignedMedia { + /// The `Link` header value naming a remote manifest, when there is one. + pub(crate) fn link(&self) -> Option { + self.manifest_url + .as_deref() + .map(|url| format!("<{url}>; rel=\"c2pa-manifest\"")) + } +} + +/// Why a `200` answer is not accepted as signed media. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResponseError { + /// The answer's media type is not the one the source was delivered as. + ContentType, + /// The answer was signed under another policy. + PolicyDigest, + /// The body is not the bytes the signed digest names. + SignedDigest, + /// A header is missing, repeated or malformed. + Malformed, + /// The manifest is remote and needs a `Link` the delivery cannot carry. + LinkUndeliverable, + /// The body carries no manifest and no reference to one. + Unsigned, +} + +/// The one value of `name`, when it appears exactly once as visible ASCII. +fn single<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> { + let mut values = headers.get_all(name).iter(); + let value = values.next()?; + if values.next().is_some() { + return None; + } + value.to_str().ok() +} + +fn valid_identifier(value: &str) -> bool { + !value.is_empty() + && value.len() <= MAX_IDENTIFIER_BYTES + && value.bytes().all(|byte| byte.is_ascii_graphic()) +} + +/// Whether `value` is a canonical absolute `https` URL that is safe inside a +/// `Link` header's angle brackets. +fn valid_https_url(value: &str) -> bool { + value.len() <= MAX_URL_BYTES + && value + .bytes() + .all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'<' | b'>' | b'"')) + && url::Url::parse(value).is_ok_and(|parsed| { + parsed.scheme() == "https" && parsed.host_str().is_some() && parsed.as_str() == value + }) +} + +/// Checks a `200` signing answer against what was sent, and keeps it only +/// when it is signed media this delivery can carry. +/// +/// # Errors +/// +/// Returns the first [`ResponseError`] the answer shows. +pub(crate) fn verify_signed_response( + headers: &HeaderMap, + body: Bytes, + expected: &Expected<'_>, +) -> Result { + let content_type = single(headers, "content-type").ok_or(ResponseError::Malformed)?; + if canonical_media_type(content_type) != expected.media_type { + return Err(ResponseError::ContentType); + } + let policy_digest = single(headers, POLICY_DIGEST_HEADER).ok_or(ResponseError::Malformed)?; + if !valid_policy_digest(policy_digest) { + return Err(ResponseError::Malformed); + } + if policy_digest != expected.policy_digest { + return Err(ResponseError::PolicyDigest); + } + let signed_sha256 = single(headers, SIGNED_SHA256_HEADER).ok_or(ResponseError::Malformed)?; + if !valid_policy_digest(signed_sha256) { + return Err(ResponseError::Malformed); + } + if body.is_empty() || sha256_hex(&body) != signed_sha256 { + return Err(ResponseError::SignedDigest); + } + let instance_id = single(headers, INSTANCE_ID_HEADER) + .filter(|value| valid_identifier(value)) + .ok_or(ResponseError::Malformed)?; + let document_id = single(headers, DOCUMENT_ID_HEADER) + .filter(|value| valid_identifier(value)) + .ok_or(ResponseError::Malformed)?; + let verification_url = single(headers, VERIFICATION_URL_HEADER) + .filter(|value| valid_https_url(value)) + .ok_or(ResponseError::Malformed)?; + let link_required = match single(headers, LINK_REQUIRED_HEADER) { + Some("true") => true, + Some("false") => false, + _ => return Err(ResponseError::Malformed), + }; + let manifest_url = headers.get(MANIFEST_URL_HEADER).map(|_| { + single(headers, MANIFEST_URL_HEADER) + .filter(|value| valid_https_url(value)) + .ok_or(ResponseError::Malformed) + }); + let changed = signed_sha256 != expected.unsigned_sha256; + let disposition = match single(headers, DISPOSITION_HEADER) { + Some("embedded") => { + // The manifest store is in the bytes, so they changed, hold the + // `c2pa` label and point nowhere else. These are consistency + // checks against a wrong answer, not a signature check: the + // signature is trusted through the authenticated endpoint and + // the digest binding above. + if manifest_url.is_some() || link_required { + return Err(ResponseError::Malformed); + } + if !changed || !contains(&body, b"c2pa") { + return Err(ResponseError::Unsigned); + } + Disposition::Embedded + } + Some("external") => { + if manifest_url.is_none() { + return Err(ResponseError::Malformed); + } + single(headers, MANIFEST_SHA256_HEADER) + .filter(|value| valid_policy_digest(value)) + .ok_or(ResponseError::Malformed)?; + if link_required && !expected.link_deliverable { + return Err(ResponseError::LinkUndeliverable); + } + // A remote manifest no `Link` names must be named by the bytes. + if !link_required && !changed { + return Err(ResponseError::Unsigned); + } + Disposition::External + } + _ => return Err(ResponseError::Malformed), + }; + let manifest_url = manifest_url.transpose()?.map(str::to_owned); + let content_type = HeaderValue::from_str(content_type).map_err(|_| ResponseError::Malformed)?; + Ok(SignedMedia { + bytes: body, + content_type, + instance_id: instance_id.to_owned(), + document_id: document_id.to_owned(), + verification_url: verification_url.to_owned(), + disposition, + manifest_url, + }) +} + +/// The failure code of an error answer, kept only as a short identifier. +pub(crate) fn error_code(body: &[u8]) -> Option { + #[derive(Deserialize)] + struct ErrorBody { + error: ErrorDetail, + } + #[derive(Deserialize)] + struct ErrorDetail { + code: String, + } + if body.len() > MAX_ERROR_BODY_BYTES { + return None; + } + let parsed: ErrorBody = serde_json::from_slice(body).ok()?; + let code = parsed.error.code; + (!code.is_empty() + && code.len() <= MAX_ERROR_CODE_BYTES + && code + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')) + .then_some(code) +} + +#[cfg(test)] +mod tests { + use http::HeaderName; + use serde_json::json; + + use super::*; + + const DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const UNSIGNED: &[u8] = b"\x89PNG\r\n\x1a\nunsigned"; + const SIGNED: &[u8] = b"\x89PNG\r\n\x1a\nunsigned-caBXjumbc2pa-manifest"; + + fn expected(unsigned_sha256: &str, link_deliverable: bool) -> Expected<'_> { + Expected { + policy_digest: DIGEST, + media_type: "image/png", + unsigned_sha256, + link_deliverable, + } + } + + fn embedded_headers(body: &[u8]) -> HeaderMap { + let mut headers = HeaderMap::new(); + for (name, value) in [ + ("content-type", "image/png".to_owned()), + (POLICY_DIGEST_HEADER, DIGEST.to_owned()), + (SIGNED_SHA256_HEADER, sha256_hex(body)), + (INSTANCE_ID_HEADER, "xmp:iid:0001".to_owned()), + (DISPOSITION_HEADER, "embedded".to_owned()), + (LINK_REQUIRED_HEADER, "false".to_owned()), + ( + VERIFICATION_URL_HEADER, + "https://verify.example.com/doc-1".to_owned(), + ), + (DOCUMENT_ID_HEADER, "doc-1".to_owned()), + ] { + headers.insert( + HeaderName::from_static(name), + HeaderValue::from_str(&value).expect("should build header"), + ); + } + headers + } + + fn external_headers(body: &[u8], link_required: bool) -> HeaderMap { + let mut headers = embedded_headers(body); + headers.insert(DISPOSITION_HEADER, HeaderValue::from_static("external")); + headers.insert( + MANIFEST_URL_HEADER, + HeaderValue::from_static("https://api.example.com/manifests/doc-1"), + ); + headers.insert( + MANIFEST_SHA256_HEADER, + HeaderValue::from_str(DIGEST).expect("should build header"), + ); + headers.insert( + LINK_REQUIRED_HEADER, + HeaderValue::from_static(if link_required { "true" } else { "false" }), + ); + headers + } + + #[test] + fn metadata_matches_the_frozen_wire_shape() { + let sha = sha256_hex(UNSIGNED); + let metadata = MediaMetadata::unmodified( + "https://publisher.example.com/news/story", + DIGEST, + SourceMetadata { + reference: None, + origin_host: None, + mime: "image/png", + sha256: &sha, + byte_length: UNSIGNED.len(), + require_embedded_manifest: false, + }, + false, + ); + let value = serde_json::to_value(&metadata).expect("should serialize metadata"); + assert_eq!( + value, + json!({ + "page_url": "https://publisher.example.com/news/story", + "expected_policy_digest": DIGEST, + "source": { + "reference": null, + "origin_host": null, + "mime": "image/png", + "sha256": sha, + "byte_length": UNSIGNED.len(), + }, + "execution_record": { + "source_sha256": sha, + "final_sha256": sha, + "complete": true, + "operations": [], + }, + "delivery": { "http_link_header": false }, + }), + "should send an unmodified, complete record with a null reference" + ); + } + + #[test] + fn an_optimizer_answer_requires_its_embedded_manifest_on_the_wire() { + let sha = sha256_hex(SIGNED); + let metadata = MediaMetadata::unmodified( + "https://publisher.example.com/images/photo.jpg", + DIGEST, + SourceMetadata { + reference: None, + origin_host: Some("origin.example.com"), + mime: "image/png", + sha256: &sha, + byte_length: SIGNED.len(), + require_embedded_manifest: true, + }, + true, + ); + let value = serde_json::to_value(&metadata).expect("should serialize metadata"); + assert_eq!( + value["source"], + json!({ + "reference": null, + "origin_host": "origin.example.com", + "mime": "image/png", + "sha256": sha, + "byte_length": SIGNED.len(), + "require_embedded_manifest": true, + }), + "should require the optimizer's embedded manifest and name no address" + ); + assert_eq!( + value["execution_record"], + json!({ + "source_sha256": sha, + "final_sha256": sha, + "complete": true, + "operations": [], + }), + "should report no operation of the edge's own" + ); + } + + #[test] + fn multipart_carries_exact_source_bytes_under_an_absent_boundary() { + let source = b"--encypher-media-binary\r\n\x00\xff"; + let built = multipart(br#"{"page_url":"x"}"#, source); + let boundary = built + .content_type + .strip_prefix("multipart/form-data; boundary=") + .expect("should declare a boundary"); + let body = built.body.as_ref(); + let source_start = body + .windows(4) + .position(|window| window == b"\r\n\r\n") + .and_then(|first| { + body[first + 4..] + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|second| first + 4 + second + 4) + }) + .expect("should find the source part body"); + let tail = format!("\r\n--{boundary}--\r\n"); + assert!(body.ends_with(tail.as_bytes()), "should close the body"); + assert_eq!( + &body[source_start..body.len() - tail.len()], + source, + "should carry the exact source bytes" + ); + assert!( + body.windows(9).any(|window| window == b"name=\"sou"), + "should name the source part" + ); + assert!( + !body.windows(7).any(|window| window == b"\"final\""), + "should send no final part for unmodified media" + ); + } + + #[test] + fn an_embedded_answer_is_accepted_only_with_matching_bytes_and_policy() { + let unsigned = sha256_hex(UNSIGNED); + let signed = verify_signed_response( + &embedded_headers(SIGNED), + Bytes::from_static(SIGNED), + &expected(&unsigned, false), + ) + .expect("should accept a verified embedded answer"); + assert_eq!(signed.bytes.as_ref(), SIGNED, "should keep the exact bytes"); + assert_eq!( + signed.disposition, + Disposition::Embedded, + "should be embedded" + ); + assert_eq!(signed.link(), None, "should need no Link"); + + let tampered = verify_signed_response( + &embedded_headers(SIGNED), + Bytes::from_static(b"\x89PNG\r\n\x1a\nc2pa-other"), + &expected(&unsigned, false), + ); + assert_eq!( + tampered.expect_err("should refuse a body the digest does not name"), + ResponseError::SignedDigest + ); + + let mut stale = embedded_headers(SIGNED); + stale.insert( + POLICY_DIGEST_HEADER, + HeaderValue::from_str(&"b".repeat(64)).expect("should build header"), + ); + assert_eq!( + verify_signed_response( + &stale, + Bytes::from_static(SIGNED), + &expected(&unsigned, false) + ) + .expect_err("should refuse another policy"), + ResponseError::PolicyDigest + ); + + let mut retyped = embedded_headers(SIGNED); + retyped.insert("content-type", HeaderValue::from_static("image/jpeg")); + assert_eq!( + verify_signed_response( + &retyped, + Bytes::from_static(SIGNED), + &expected(&unsigned, false) + ) + .expect_err("should refuse a changed media type"), + ResponseError::ContentType + ); + } + + #[test] + fn unchanged_bytes_are_never_accepted_as_signed() { + let unsigned = sha256_hex(UNSIGNED); + assert_eq!( + verify_signed_response( + &embedded_headers(UNSIGNED), + Bytes::from_static(UNSIGNED), + &expected(&unsigned, false), + ) + .expect_err("should refuse an unsigned embedded answer"), + ResponseError::Unsigned + ); + assert_eq!( + verify_signed_response( + &external_headers(UNSIGNED, false), + Bytes::from_static(UNSIGNED), + &expected(&unsigned, true), + ) + .expect_err("should refuse a remote manifest nothing names"), + ResponseError::Unsigned + ); + } + + #[test] + fn a_remote_manifest_needs_a_deliverable_link() { + let unsigned = sha256_hex(UNSIGNED); + assert_eq!( + verify_signed_response( + &external_headers(UNSIGNED, true), + Bytes::from_static(UNSIGNED), + &expected(&unsigned, false), + ) + .expect_err("should refuse a Link the delivery cannot carry"), + ResponseError::LinkUndeliverable + ); + let signed = verify_signed_response( + &external_headers(UNSIGNED, true), + Bytes::from_static(UNSIGNED), + &expected(&unsigned, true), + ) + .expect("should accept a remote manifest the delivery links"); + assert_eq!( + signed.link().as_deref(), + Some("; rel=\"c2pa-manifest\""), + "should name the manifest in a Link" + ); + } + + #[test] + fn repeated_or_malformed_signing_headers_are_refused() { + let unsigned = sha256_hex(UNSIGNED); + let mut repeated = embedded_headers(SIGNED); + repeated.append(INSTANCE_ID_HEADER, HeaderValue::from_static("xmp:iid:0002")); + let mut bad_url = embedded_headers(SIGNED); + bad_url.insert( + VERIFICATION_URL_HEADER, + HeaderValue::from_static("http://verify.example.com/doc-1"), + ); + let mut bad_disposition = embedded_headers(SIGNED); + bad_disposition.insert(DISPOSITION_HEADER, HeaderValue::from_static("sidecar")); + for headers in [repeated, bad_url, bad_disposition] { + assert_eq!( + verify_signed_response( + &headers, + Bytes::from_static(SIGNED), + &expected(&unsigned, false) + ) + .expect_err("should refuse malformed headers"), + ResponseError::Malformed + ); + } + } + + #[test] + fn common_misspellings_name_the_media_type_the_service_lists() { + for (declared, canonical) in [ + ("image/jpg", "image/jpeg"), + ("audio/x-aac", "audio/aac"), + ("Audio/AACP; codecs=aac", "audio/aac"), + ("audio/aac", "audio/aac"), + ("audio/mp4", "audio/mp4"), + ] { + assert_eq!(canonical_media_type(declared), canonical, "{declared}"); + } + } + + #[test] + fn source_references_are_sent_only_when_canonical_and_on_the_claim_host() { + let host = "publisher.example.com"; + assert!( + safe_source_reference("https://publisher.example.com/images/a.png", host), + "should send a canonical https address" + ); + for unsafe_reference in [ + "https://publisher.example.com/images/a.png?token=secret", + "https://user:pass@publisher.example.com/images/a.png", + "https://publisher.example.com/images/a.png#frag", + "http://publisher.example.com/images/a.png", + "https://publisher.example.com:8443/images/a.png", + "https://origin.example.com/images/a.png", + "https://PUBLISHER.example.com/images/a.png", + "https://publisher.example.com/", + ] { + assert!( + !safe_source_reference(unsafe_reference, host), + "should never send {unsafe_reference}" + ); + } + } + + #[test] + fn error_codes_are_read_as_short_identifiers_only() { + let body = serde_json::to_vec(&json!({ + "success": false, + "error": {"code": "TRUSTED_SERVER_POLICY_CHANGED", "message": "stale"}, + "correlation_id": "c-1", + })) + .expect("should serialize"); + assert_eq!( + error_code(&body).as_deref(), + Some("TRUSTED_SERVER_POLICY_CHANGED") + ); + let injected = + serde_json::to_vec(&json!({"error": {"code": "a b\nc"}})).expect("should serialize"); + assert_eq!(error_code(&injected), None, "should drop an unsafe code"); + } +} diff --git a/crates/provenance/encypher/src/preparation/edge.rs b/crates/provenance/encypher/src/preparation/edge.rs new file mode 100644 index 0000000000..00cecfe78c --- /dev/null +++ b/crates/provenance/encypher/src/preparation/edge.rs @@ -0,0 +1,1397 @@ +//! The delivery instance's side of the preparation protocol: its internal +//! snapshot, result and marker routes, and the public source route. +//! +//! Every control route is authenticated with the publication scope's +//! secret, compared in constant time, and fixed to the one configured +//! deployment and its routes. None accepts a hostname, fetch headers, a +//! source credential or a signer. +//! +//! A snapshot is read through the same reader pipeline every reader takes: +//! the route asks the instance's own public address for the configured +//! target, carrying a short-lived capability for exactly that path. The +//! module's request preparer verifies and removes it before anything can +//! forward it, leaving an observation that changes nothing about how the +//! request is answered, so the same template, cache, origin and +//! transformations serve it. Only this module's own signing and lookup are +//! suppressed for it, and its answer reports what the origin's own headers +//! said, authenticated for the worker's nonce, apart from its private +//! transport headers. +//! +//! A result publication never trusts what the worker says it signed. The +//! route recomputes the identity from the item, recovers the exact +//! completed receipt from the authenticated API without signing, checks it +//! against the actual source bytes and, for embedded media, builds the +//! output itself and requires the worker's to be exactly it. Only then is +//! the record stored, under its immutable identity, and never over a +//! retirement marker. + +use std::cell::RefCell; +use std::time::Duration; + +use async_trait::async_trait; +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use bytes::Bytes; +use edgezero_core::body::Body as EdgeBody; +use edgezero_core::error::EdgeError; +use error_stack::Report; +use http::{HeaderValue, Method, Request, Response, StatusCode, header}; +use serde_json::json; +use sha2::Digest as _; + +use trusted_server_core::error::TrustedServerError; +use trusted_server_core::integrations::{ + IntegrationEndpoint, IntegrationProxy, IntegrationRequestState, + ensure_integration_backend_with_timeout, +}; +use trusted_server_core::media_signing::SnapshotFacts; +use trusted_server_core::middleware::ResponseHeader; +use trusted_server_core::module_context::ModuleCall; +use trusted_server_core::platform::{KvError, PlatformHttpRequest, RuntimeServices}; +use trusted_server_core::settings::Settings; + +use super::pipeline::{ + self, ApiAnswer, ApiRequest, ApiTransport, Failure, FailureClass, ProbeArtifactCache, + SourceReader, +}; +use super::protocol::{ + CAPABILITY_HEADER, Carrier, EnvelopeItem, FACTS_HEADER, LargeLocator, MARKERS_PREFIX, + MarkerEnvelope, MediaIdentity, PREPARATION_PROTOCOL, PUBLIC_SOURCE_PREFIX, PublishedRecord, + PublishedResult, RESULTS_PREFIX, ResultEnvelope, RetirementMarker, SNAPSHOT_META_HEADER, + SNAPSHOT_PATH, SnapshotMeta, SnapshotMode, SnapshotOrigin, SnapshotRequest, TOKEN_HEADER, + Target, TextIdentity, WORKER_SOURCE_PREFIX, facts_header, is_digest, marker_key, result_key, + snapshot_capability, token_matches, verify_facts_header, verify_snapshot_capability, +}; +use super::{Execution, PreparationConfig}; +use crate::media_api::{canonical_media_type, hex_lower, sha256_hex}; +use crate::processor::Signer; +use crate::{EncypherConfig, INTEGRATION_ID, MODULE}; + +/// The largest result envelope: an embedded item's source and output, +/// base64, with its plan. +const MAX_ENVELOPE_BYTES: usize = 24 * 1024 * 1024; +/// The largest one range a snapshot reads. +const MAX_RANGE_BYTES: u64 = 8 * 1024 * 1024; +/// The aligned block a pinned source answers the probe's small structure +/// reads from. +const PINNED_BLOCK_BYTES: u64 = 64 * 1024; +/// The largest worker answer for a public source. +const MAX_PUBLIC_SOURCE_BYTES: usize = 8 * 1024 * 1024; +/// How long one loopback snapshot read may wait for its first byte. +const LOOPBACK_TIMEOUT: Duration = Duration::from_secs(30); +/// The cache key prefix of verified probe artifacts, the same the large +/// lane keeps them under. +const PROBE_KEY_PREFIX: &str = "encypher:probe:v1"; + +/// What the module's request preparer leaves on a verified snapshot read: +/// the nonce its facts are authenticated for. Left as an observation, so +/// the read takes every reader's path. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SnapshotGrant { + pub(crate) nonce: String, +} + +fn unix_now() -> u64 { + web_time::SystemTime::now() + .duration_since(web_time::UNIX_EPOCH) + .map_or(0, |elapsed| elapsed.as_secs()) +} + +/// The asynchronous mode's settings, where the deployment runs it. +fn async_config(settings: &Settings) -> Option<(EncypherConfig, PreparationConfig)> { + let config = settings + .module_config::(MODULE) + .ok() + .flatten()?; + if config.execution != Execution::AsyncPrepared { + return None; + } + let preparation = config.preparation.clone()?; + Some((config, preparation)) +} + +/// The module's request preparer: removes every snapshot capability from +/// every request, whether or not the module runs, so none ever reaches an +/// origin, and leaves a [`SnapshotGrant`] observation only for one that +/// verifies for exactly this path and query under the publication secret. +/// A client cannot set the grant: it exists only as request state this +/// preparer writes. +/// +/// # Errors +/// +/// Never; a capability that does not verify is dropped. +pub(crate) fn prepare_request( + settings: &Settings, + request: &mut Request, +) -> Result<(), Report> { + let value = { + let mut values = request.headers().get_all(CAPABILITY_HEADER).iter(); + match (values.next(), values.next()) { + (Some(value), None) => Some(value.clone()), + _ => None, + } + }; + request.headers_mut().remove(CAPABILITY_HEADER); + let Some(value) = value else { + return Ok(()); + }; + let Ok(value) = value.to_str() else { + return Ok(()); + }; + let Some((_, preparation)) = async_config(settings) else { + return Ok(()); + }; + let prefix = request + .extensions() + .get::() + .map(|prefix| prefix.as_str().to_owned()) + .unwrap_or_default(); + let path_and_query = match request.uri().query() { + Some(query) => format!("{prefix}{}?{query}", request.uri().path()), + None => format!("{prefix}{}", request.uri().path()), + }; + if let Some(nonce) = verify_snapshot_capability( + preparation.publication_secret.expose(), + &path_and_query, + value, + unix_now(), + ) { + IntegrationRequestState::observe(request, INTEGRATION_ID, SnapshotGrant { nonce }); + } + Ok(()) +} + +/// The facts header a snapshot answer carries, authenticated for its +/// grant's nonce. +pub(crate) fn snapshot_facts( + preparation: &PreparationConfig, + grant: &SnapshotGrant, + origin: &SnapshotOrigin, +) -> Option { + let value = facts_header( + preparation.publication_secret.expose(), + &grant.nonce, + origin, + ); + Some(( + http::HeaderName::from_static(FACTS_HEADER), + HeaderValue::from_str(&value).ok()?, + )) +} + +/// The origin facts core reports for a media snapshot, as the protocol +/// carries them. +pub(crate) fn media_snapshot_origin(facts: &SnapshotFacts) -> SnapshotOrigin { + SnapshotOrigin { + status: facts.status.as_u16(), + private: facts.private, + no_transform: facts.no_transform, + manifest_link: facts.manifest_link, + content_encoding: facts.content_encoding.clone(), + origin_host: facts.origin_host.clone(), + origin_identity_sha256: facts.origin_identity_sha256.clone(), + source_reference: facts.source_reference.clone(), + } +} + +/// The Encypher API, as the delivery instance's platform client reaches +/// it, for receipt recovery and probe artifacts only. +pub(crate) struct EdgeApi<'a> { + pub(crate) config: &'a EncypherConfig, + pub(crate) services: &'a RuntimeServices, +} + +impl ApiTransport for EdgeApi<'_> { + async fn exchange(&self, request: ApiRequest) -> Result { + let backend = ensure_integration_backend_with_timeout( + self.services, + &self.config.api_base_url, + INTEGRATION_ID, + Duration::from_millis(self.config.timeout_ms), + ) + .map_err(|_| Failure::new(FailureClass::Transient, "backend"))?; + let mut builder = Request::builder().method(request.method).uri(format!( + "{}{}", + self.config.api_base_url.trim_end_matches('/'), + request.path_and_query + )); + if request.credentialed { + let mut token = HeaderValue::from_str(self.config.worker_token.expose().trim()) + .map_err(|_| Failure::new(FailureClass::Transient, "request_build"))?; + token.set_sensitive(true); + builder = builder.header("x-encypher-worker-token", token); + } + if let Some(key) = &request.idempotency_key { + builder = builder.header("idempotency-key", key.as_str()); + } + let body = match request.body { + Some(body) => { + builder = builder.header(header::CONTENT_TYPE, "application/json"); + EdgeBody::from(body) + } + None => EdgeBody::empty(), + }; + let http_request = builder + .body(body) + .map_err(|_| Failure::new(FailureClass::Transient, "request_build"))?; + let limit = request.max_response_bytes.saturating_add(1); + let answer = self + .services + .http_client() + .send( + PlatformHttpRequest::new(http_request, backend) + .with_cache_bypass() + .with_max_response_body_bytes(limit), + ) + .await + .map_err(|_| Failure::new(FailureClass::Transient, "http_error"))?; + let status = answer.response.status().as_u16(); + let body = answer + .response + .into_body() + .into_bytes_bounded(limit) + .await + .map_err(|_| Failure::new(FailureClass::Transient, "response_too_large"))?; + Ok(ApiAnswer { status, body }) + } +} + +/// Verified probe artifacts kept in the runtime KV store. +pub(crate) struct KvProbeCache<'a>(pub(crate) &'a RuntimeServices); + +impl ProbeArtifactCache for KvProbeCache<'_> { + async fn get(&self, sha256: &str) -> Option { + self.0 + .kv_store() + .get_bytes(&format!("{PROBE_KEY_PREFIX}:{sha256}")) + .await + .ok() + .flatten() + } + + async fn put(&self, sha256: &str, artifact: Bytes) -> Result<(), Failure> { + // The edge keeps artifacts only to avoid fetching them again; the + // load goes on from the verified bytes in hand when the write fails. + if self + .0 + .kv_store() + .put_bytes(&format!("{PROBE_KEY_PREFIX}:{sha256}"), artifact) + .await + .is_err() + { + log::debug!("encypher: probe release cache write failed"); + } + Ok(()) + } +} + +/// The module's preparation routes on the delivery instance. +pub(crate) struct PreparationRoutes { + signer: std::sync::Arc, +} + +impl PreparationRoutes { + /// The routes of `signer`, holding releases to the production key. + pub(crate) fn new(signer: std::sync::Arc) -> Self { + Self { signer } + } +} + +impl std::fmt::Debug for PreparationRoutes { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("PreparationRoutes") + .finish_non_exhaustive() + } +} + +fn json_response(status: StatusCode, value: &serde_json::Value) -> Response { + Response::builder() + .status(status) + .header(header::CONTENT_TYPE, "application/json") + .header(header::CACHE_CONTROL, "private, no-store") + .body(EdgeBody::from(value.to_string())) + .expect("should build a JSON response") +} + +fn refusal(status: StatusCode, code: &str) -> Response { + json_response(status, &json!({"error": {"code": code}})) +} + +fn failure_response(failure: &Failure) -> Response { + let status = match failure.class { + FailureClass::Transient => StatusCode::SERVICE_UNAVAILABLE, + FailureClass::Missing => StatusCode::GONE, + FailureClass::Privacy => StatusCode::FORBIDDEN, + FailureClass::PolicyChanged => StatusCode::CONFLICT, + FailureClass::Integrity | FailureClass::Unsupported | FailureClass::ExistingProvenance => { + StatusCode::UNPROCESSABLE_ENTITY + } + }; + log::info!( + "encypher: preparation publication refused code={}", + failure.code + ); + refusal(status, &failure.code) +} + +async fn read_body(request: Request, limit: usize) -> Option { + request + .into_body() + .into_bytes_bounded(limit.saturating_add(1)) + .await + .ok() + .filter(|body| body.len() <= limit) +} + +impl PreparationRoutes { + fn config(&self) -> Option<(&EncypherConfig, &PreparationConfig)> { + let config = self.signer.config(); + Some((config, self.signer.preparation()?)) + } + + async fn route( + &self, + request: Request, + services: &RuntimeServices, + ) -> Result, Report> { + let Some((config, preparation)) = self.config() else { + return Ok(refusal(StatusCode::NOT_FOUND, "not_found")); + }; + let path = request.uri().path().to_owned(); + if request.method() == Method::GET { + if let Some(sha256) = path.strip_prefix(PUBLIC_SOURCE_PREFIX) { + return Ok(self.public_source(services, preparation, sha256).await); + } + return Ok(refusal(StatusCode::NOT_FOUND, "not_found")); + } + let presented = request + .headers() + .get(TOKEN_HEADER) + .and_then(|value| value.to_str().ok()) + .unwrap_or_default(); + if !token_matches(preparation.publication_secret.expose(), presented) { + return Ok(refusal(StatusCode::UNAUTHORIZED, "unauthorized")); + } + if request.method() == Method::POST && path == SNAPSHOT_PATH { + return Ok(self.snapshot(services, config, preparation, request).await); + } + if request.method() == Method::PUT { + if let Some(identity) = path.strip_prefix(RESULTS_PREFIX).map(str::to_owned) { + return Ok(self + .publish(services, config, preparation, &identity, request) + .await); + } + if let Some(identity) = path.strip_prefix(MARKERS_PREFIX).map(str::to_owned) { + return Ok(self.retire(services, preparation, &identity, request).await); + } + } + Ok(refusal(StatusCode::NOT_FOUND, "not_found")) + } + + /// Reads one configured target through the reader pipeline. + async fn snapshot( + &self, + services: &RuntimeServices, + config: &EncypherConfig, + preparation: &PreparationConfig, + request: Request, + ) -> Response { + let Some(body) = read_body(request, 4096).await else { + return refusal(StatusCode::PAYLOAD_TOO_LARGE, "request_too_large"); + }; + let Ok(asked) = serde_json::from_slice::(&body) else { + return refusal(StatusCode::BAD_REQUEST, "malformed_request"); + }; + if asked.protocol != PREPARATION_PROTOCOL + || asked.deployment_id != preparation.deployment_id + { + return refusal(StatusCode::FORBIDDEN, "wrong_deployment"); + } + if !asked.target.is_well_formed() + || !preparation.admits_path(&asked.target.path) + || preparation.variants.admit(&asked.target.variant).as_deref() + != Some(asked.target.variant.as_str()) + { + return refusal(StatusCode::FORBIDDEN, "target_not_configured"); + } + if asked.nonce.len() != 32 || !asked.nonce.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return refusal(StatusCode::BAD_REQUEST, "nonce_invalid"); + } + let (limit, range) = match &asked.read { + SnapshotMode::Whole => (config.max_document_bytes.max(config.max_media_bytes), None), + SnapshotMode::Head => (0, None), + SnapshotMode::Range { + first, + last, + if_match, + } => { + let Some(length) = last + .checked_sub(*first) + .and_then(|span| span.checked_add(1)) + else { + return refusal(StatusCode::BAD_REQUEST, "range_invalid"); + }; + if length > MAX_RANGE_BYTES { + return refusal(StatusCode::BAD_REQUEST, "range_too_large"); + } + ( + usize::try_from(length).unwrap_or(usize::MAX), + Some((*first, *last, if_match.clone())), + ) + } + }; + let path_and_query = asked.target.path_and_query(); + let capability = snapshot_capability( + preparation.publication_secret.expose(), + &path_and_query, + &asked.nonce, + unix_now() + super::protocol::CAPABILITY_LIFETIME_SECONDS, + ); + let answer = match loopback_read( + services, + preparation, + &path_and_query, + &capability, + range.as_ref(), + limit, + ) + .await + { + Ok(answer) => answer, + Err(code) => return refusal(StatusCode::BAD_GATEWAY, code), + }; + let (parts, body) = answer.into_parts(); + let Some(origin) = parts + .headers + .get(FACTS_HEADER) + .and_then(|value| value.to_str().ok()) + .and_then(|value| { + verify_facts_header(preparation.publication_secret.expose(), &asked.nonce, value) + }) + else { + // The module did not observe the read: the target is not one a + // [[serve]] entry names it for, or the answer is not this read's. + return refusal(StatusCode::UNPROCESSABLE_ENTITY, "snapshot_unobserved"); + }; + if origin.private { + return refusal(StatusCode::FORBIDDEN, "source_private"); + } + if origin.manifest_link { + return refusal( + StatusCode::UNPROCESSABLE_ENTITY, + "source_has_provenance_link", + ); + } + if origin.no_transform { + return refusal(StatusCode::UNPROCESSABLE_ENTITY, "no_transform"); + } + if !origin.content_encoding.is_empty() { + return refusal(StatusCode::UNPROCESSABLE_ENTITY, "content_encoded"); + } + let expected_status = if range.is_some() { + StatusCode::PARTIAL_CONTENT + } else { + StatusCode::OK + }; + if parts.status != expected_status && !(range.is_some() && parts.status == StatusCode::OK) { + return refusal(StatusCode::UNPROCESSABLE_ENTITY, "source_status"); + } + let etag = single_strong_etag(&parts.headers); + if let Some((_, _, if_match)) = &range + && etag.as_deref() != Some(if_match.as_str()) + { + return refusal(StatusCode::PRECONDITION_FAILED, "source_version_changed"); + } + let length = if let Some((first, last, _)) = &range { + let Some(total) = pinned_range_length(parts.status, &parts.headers, *first, *last) + else { + return refusal(StatusCode::UNPROCESSABLE_ENTITY, "source_range_invalid"); + }; + Some(total) + } else { + parts + .headers + .get(header::CONTENT_LENGTH) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.trim().parse().ok()) + }; + let meta = SnapshotMeta { + protocol: PREPARATION_PROTOCOL.to_owned(), + page_url: format!("https://{}{}", self.signer.public_host(), asked.target.path), + content_type: parts + .headers + .get(header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .unwrap_or_default() + .to_owned(), + etag, + length, + origin, + }; + // The read's headers arrive before its body, so nothing above has + // read a byte of it. A head snapshot drops the body unread; a whole + // one declared past the bound is refused unread; every other body is + // read only up to the bound. A failed read stays a failed read and is + // never reported as an oversized source. + let bytes = if limit == 0 { + Bytes::new() + } else if range.is_none() + && length.is_some_and(|length| length > u64::try_from(limit).unwrap_or(u64::MAX)) + { + return refusal(StatusCode::PAYLOAD_TOO_LARGE, "snapshot_too_large"); + } else { + match body.into_bytes_bounded(limit).await { + Ok(bytes) if bytes.len() <= limit => bytes, + Ok(_) | Err(EdgeError::BadRequest { .. }) => { + return refusal(StatusCode::PAYLOAD_TOO_LARGE, "snapshot_too_large"); + } + Err(_) => return refusal(StatusCode::BAD_GATEWAY, "loopback_failed"), + } + }; + if let Some((first, last, _)) = &range + && u64::try_from(bytes.len()).ok() != Some(last - first + 1) + { + return refusal(StatusCode::BAD_GATEWAY, "range_short"); + } + let encoded = STANDARD.encode(serde_json::to_vec(&meta).unwrap_or_default()); + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/octet-stream") + .header(header::CACHE_CONTROL, "private, no-store") + .header(SNAPSHOT_META_HEADER, encoded) + .body(EdgeBody::Once(bytes)) + .expect("should build a snapshot response") + } + + /// Verifies one result through the authenticated API and stores it + /// under its immutable identity. + async fn publish( + &self, + services: &RuntimeServices, + config: &EncypherConfig, + preparation: &PreparationConfig, + identity: &str, + request: Request, + ) -> Response { + if !is_digest(identity) { + return refusal(StatusCode::BAD_REQUEST, "identity_invalid"); + } + let Some(body) = read_body(request, MAX_ENVELOPE_BYTES).await else { + return refusal(StatusCode::PAYLOAD_TOO_LARGE, "result_too_large"); + }; + let Ok(envelope) = serde_json::from_slice::(&body) else { + return refusal(StatusCode::BAD_REQUEST, "malformed_request"); + }; + if envelope.protocol != PREPARATION_PROTOCOL + || envelope.deployment_id != preparation.deployment_id + || envelope.identity != identity + { + return refusal(StatusCode::FORBIDDEN, "wrong_deployment"); + } + if preparation.variants.admit(&envelope.variant).as_deref() + != Some(envelope.variant.as_str()) + { + return refusal(StatusCode::FORBIDDEN, "variant_not_admitted"); + } + let kv = services.kv_store(); + match kv.get_bytes(&marker_key(identity)).await { + Ok(None) => {} + Ok(Some(_)) => return refusal(StatusCode::CONFLICT, "retired"), + Err(_) => return refusal(StatusCode::SERVICE_UNAVAILABLE, "marker_unavailable"), + } + let result = match self + .verify_envelope(services, config, preparation, &envelope) + .await + { + Ok(result) => result, + Err(failure) => return failure_response(&failure), + }; + // A marker written while the receipt was being checked still wins. + // Readers refuse a retired identity whatever its result says, so + // this only keeps a known-retired result from being written at all. + match kv.get_bytes(&marker_key(identity)).await { + Ok(None) => {} + Ok(Some(_)) => return refusal(StatusCode::CONFLICT, "retired"), + Err(_) => return refusal(StatusCode::SERVICE_UNAVAILABLE, "marker_unavailable"), + } + let record = PublishedRecord { + protocol: PREPARATION_PROTOCOL.to_owned(), + identity: identity.to_owned(), + retirement_epoch: envelope.retirement_epoch, + result, + }; + let Ok(encoded) = serde_json::to_vec(&record) else { + return refusal(StatusCode::INTERNAL_SERVER_ERROR, "encode"); + }; + match kv + .put_bytes(&result_key(identity), Bytes::from(encoded)) + .await + { + Ok(()) => { + log::info!("encypher: preparation result published"); + json_response( + StatusCode::OK, + &json!({"protocol": PREPARATION_PROTOCOL, "published": identity}), + ) + } + Err(KvError::Unavailable) => refusal(StatusCode::SERVICE_UNAVAILABLE, "kv_unavailable"), + Err(_) => refusal(StatusCode::SERVICE_UNAVAILABLE, "kv_error"), + } + } + + /// The record an envelope supports, recomputing its identity and + /// checking the API's own receipt. + async fn verify_envelope( + &self, + services: &RuntimeServices, + config: &EncypherConfig, + preparation: &PreparationConfig, + envelope: &ResultEnvelope, + ) -> Result { + let api = EdgeApi { config, services }; + let context = self.signer.credential_context(); + let refused = |code: &str| Failure::new(FailureClass::Integrity, code); + match &envelope.item { + EnvelopeItem::Text { + request, + idempotency_key, + } => { + self.require_page_url(preparation, &request.page_url)?; + let identity = TextIdentity { + credential_context: context, + policy_digest: &request.expected_policy_digest, + page_url: &request.page_url, + variant: &envelope.variant, + text: &request.text, + document_title: request.document_title.as_deref(), + boundary_selector: &request.boundary_selector, + } + .digest(); + if identity != envelope.identity { + return Err(refused("identity_mismatch")); + } + let signed = pipeline::recover_text(&api, request, idempotency_key).await?; + Ok(PublishedResult::Text { + plan: signed.plan, + manifest_url: signed.manifest_url, + scalars: request.text.chars().count(), + }) + } + EnvelopeItem::Media { + planned, + source_b64, + } => { + let request = &planned.request; + self.require_page_url(preparation, &request.page_url)?; + if request.carrier() != Carrier::External { + return Err(refused("carrier_mismatch")); + } + let identity = MediaIdentity { + credential_context: context, + policy_digest: &request.expected_policy_digest, + page_url: &request.page_url, + variant: &envelope.variant, + carrier: Carrier::External, + mime: &canonical_media_type(&request.source.mime), + source_sha256: &request.source.sha256, + } + .digest(); + if identity != envelope.identity { + return Err(refused("identity_mismatch")); + } + let source = STANDARD + .decode(source_b64) + .map_err(|_| refused("source_invalid"))?; + if sha256_hex(&source) != request.source.sha256 + || u64::try_from(source.len()).ok() != Some(request.source.byte_length) + { + return Err(refused("source_digest_mismatch")); + } + let recovered = + pipeline::recover_media(&api, request, &planned.idempotency_key).await?; + let probe = + pipeline::load_release(&api, &KvProbeCache(services), &planned.release).await?; + // Provenance the source carries is kept, whatever the + // worker's disclosure left out. + pipeline::inspect_external_source(&probe, request, source.as_slice()).await?; + pipeline::verify_external_receipt(&probe, planned, &recovered).await + } + EnvelopeItem::Embedded { + planned, + source_b64, + output_b64, + } => { + let request = &planned.request; + self.require_page_url(preparation, &request.page_url)?; + if request.carrier() != Carrier::Embedded { + return Err(refused("carrier_mismatch")); + } + let identity = MediaIdentity { + credential_context: context, + policy_digest: &request.expected_policy_digest, + page_url: &request.page_url, + variant: &envelope.variant, + carrier: Carrier::Embedded, + mime: &request.source.mime, + source_sha256: &request.source.sha256, + } + .digest(); + if identity != envelope.identity { + return Err(refused("identity_mismatch")); + } + let source = STANDARD + .decode(source_b64) + .map_err(|_| refused("source_invalid"))?; + let output = STANDARD + .decode(output_b64) + .map_err(|_| refused("output_invalid"))?; + if source.len() > config.max_media_bytes + || output.len() > config.max_media_bytes.saturating_mul(2) + { + return Err(refused("output_too_large")); + } + // The bytes the worker sent are the source only where they + // are exactly the ones the identity binds; checked before the + // API is asked anything. + if sha256_hex(&source) != request.source.sha256 + || u64::try_from(source.len()).ok() != Some(request.source.byte_length) + { + return Err(refused("source_digest_mismatch")); + } + let recovered = + pipeline::recover_media(&api, request, &planned.idempotency_key).await?; + let probe = + pipeline::load_release(&api, &KvProbeCache(services), &planned.release).await?; + pipeline::verify_embedded_receipt(&probe, planned, &recovered, &source, &output) + .await + } + EnvelopeItem::Large { + planned, + etag, + origin_identity_sha256, + } => { + let request = &planned.request; + self.require_page_url(preparation, &request.page_url)?; + // The worker's digest is only what it says it read; every + // pinned read below must report the same configured source + // in its own authenticated facts. + if !is_digest(origin_identity_sha256) { + return Err(refused("origin_identity_invalid")); + } + let identity = LargeLocator { + credential_context: context, + policy_digest: &request.expected_policy_digest, + reader_url: &request.page_url, + variant: &envelope.variant, + origin_host: &request.source.origin_host, + origin_identity_sha256, + mime: &request.source.mime, + etag, + length: request.source.byte_length, + } + .digest(); + if identity != envelope.identity { + return Err(refused("identity_mismatch")); + } + let path = url::Url::parse(&request.page_url) + .map(|url| url.path().to_owned()) + .map_err(|_| refused("page_url_invalid"))?; + let target = Target { + path, + variant: envelope.variant.clone(), + }; + if !target.is_well_formed() + || !preparation.admits_path(&target.path) + || preparation.variants.admit(&target.variant).as_deref() + != Some(target.variant.as_str()) + { + return Err(refused("target_invalid")); + } + if request.source.byte_length > config.max_large_media_bytes { + return Err(Failure::new(FailureClass::Unsupported, "media_too_large")); + } + let path_and_query = target.path_and_query(); + let version = PinnedVersion::new( + services, + preparation, + request, + &path_and_query, + etag, + origin_identity_sha256, + )?; + // Provenance the pinned version carries is kept, whatever the + // worker's disclosure left out. It is inspected first, through + // the same pinned reads, so such a source costs no full read. + let probe = + pipeline::load_release(&api, &KvProbeCache(services), &planned.release).await?; + pipeline::inspect_external_source(&probe, request, &PinnedSource::new(&version)) + .await?; + // The worker's digest is never taken on trust: every byte of + // the version this tag names is read again here, in pinned + // ranges through the reader pipeline, and hashed. A changed + // version or a different digest refuses the result. This is + // the control path; readers never hash. + let digest = version.digest().await?; + if digest != request.source.sha256 || planned.digest != request.source.sha256 { + return Err(refused("source_digest_mismatch")); + } + let recovered = + pipeline::recover_media(&api, request, &planned.idempotency_key).await?; + match pipeline::verify_external_receipt(&probe, planned, &recovered).await? { + PublishedResult::External { + source_sha256, + headers, + } => Ok(PublishedResult::Large { + sha256: source_sha256, + length: request.source.byte_length, + headers, + }), + _ => Err(refused("receipt_invalid")), + } + } + } + } + + /// Refuses a page address outside this deployment's public host and + /// configured routes. + fn require_page_url( + &self, + preparation: &PreparationConfig, + page_url: &str, + ) -> Result<(), Failure> { + let refused = || Failure::new(FailureClass::Integrity, "page_url_outside_deployment"); + let parsed = url::Url::parse(page_url).map_err(|_| refused())?; + let host = parsed.host_str().ok_or_else(refused)?; + if parsed.scheme() != "https" + || !self.signer.claims_host(host) + || parsed.query().is_some() + || parsed.fragment().is_some() + || !preparation.admits_path(parsed.path()) + { + return Err(refused()); + } + Ok(()) + } + + /// Writes an identity's retirement marker and drops its result. The + /// marker is a key of its own, which no result publication touches. + async fn retire( + &self, + services: &RuntimeServices, + preparation: &PreparationConfig, + identity: &str, + request: Request, + ) -> Response { + if !is_digest(identity) { + return refusal(StatusCode::BAD_REQUEST, "identity_invalid"); + } + let Some(body) = read_body(request, 4096).await else { + return refusal(StatusCode::PAYLOAD_TOO_LARGE, "request_too_large"); + }; + let Ok(envelope) = serde_json::from_slice::(&body) else { + return refusal(StatusCode::BAD_REQUEST, "malformed_request"); + }; + if envelope.protocol != PREPARATION_PROTOCOL + || envelope.deployment_id != preparation.deployment_id + || envelope.identity != identity + { + return refusal(StatusCode::FORBIDDEN, "wrong_deployment"); + } + let kv = services.kv_store(); + let existing = match kv.get_bytes(&marker_key(identity)).await { + Ok(existing) => existing + .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) + .map_or(0, |marker| marker.epoch), + Err(_) => return refusal(StatusCode::SERVICE_UNAVAILABLE, "marker_unavailable"), + }; + let marker = RetirementMarker { + epoch: existing.max(envelope.epoch), + }; + let Ok(encoded) = serde_json::to_vec(&marker) else { + return refusal(StatusCode::INTERNAL_SERVER_ERROR, "encode"); + }; + if kv + .put_bytes(&marker_key(identity), Bytes::from(encoded)) + .await + .is_err() + { + return refusal(StatusCode::SERVICE_UNAVAILABLE, "marker_write_failed"); + } + if kv.delete(&result_key(identity)).await.is_err() { + log::debug!("encypher: retired result could not be deleted; the marker refuses it"); + } + json_response( + StatusCode::OK, + &json!({"protocol": PREPARATION_PROTOCOL, "acknowledged": identity, "epoch": marker.epoch}), + ) + } + + /// Serves one explicitly public source snapshot by its digest, from the + /// worker, which holds it only after page and asset admission. Exact + /// stored bytes, never rendered or sniffed by a browser. + async fn public_source( + &self, + services: &RuntimeServices, + preparation: &PreparationConfig, + sha256: &str, + ) -> Response { + if !is_digest(sha256) { + return refusal(StatusCode::NOT_FOUND, "not_found"); + } + let Ok(backend) = ensure_integration_backend_with_timeout( + services, + &preparation.worker_url, + INTEGRATION_ID, + LOOPBACK_TIMEOUT, + ) else { + return refusal(StatusCode::SERVICE_UNAVAILABLE, "worker_unavailable"); + }; + let Ok(mut token) = HeaderValue::from_str(preparation.publication_secret.expose().trim()) + else { + return refusal(StatusCode::SERVICE_UNAVAILABLE, "worker_unavailable"); + }; + token.set_sensitive(true); + let Ok(request) = Request::builder() + .method(Method::GET) + .uri(format!( + "{}{WORKER_SOURCE_PREFIX}{sha256}", + preparation.worker_url.trim_end_matches('/') + )) + .header(TOKEN_HEADER, token) + .body(EdgeBody::empty()) + else { + return refusal(StatusCode::SERVICE_UNAVAILABLE, "worker_unavailable"); + }; + let limit = MAX_PUBLIC_SOURCE_BYTES.saturating_add(1); + let Ok(answer) = services + .http_client() + .send( + PlatformHttpRequest::new(request, backend) + .with_cache_bypass() + .with_max_response_body_bytes(limit), + ) + .await + else { + return refusal(StatusCode::SERVICE_UNAVAILABLE, "worker_unavailable"); + }; + if answer.response.status() != StatusCode::OK { + return refusal(StatusCode::NOT_FOUND, "not_found"); + } + let content_type = answer + .response + .headers() + .get(header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .map(canonical_media_type) + .filter(|media_type| crate::data_uri::is_media_type(media_type)); + let Some(content_type) = content_type else { + return refusal(StatusCode::NOT_FOUND, "not_found"); + }; + let Ok(bytes) = answer.response.into_body().into_bytes_bounded(limit).await else { + return refusal(StatusCode::NOT_FOUND, "not_found"); + }; + // The worker answers only an admitted digest; the bytes must still + // be exactly the ones the address names. + if bytes.len() > MAX_PUBLIC_SOURCE_BYTES || sha256_hex(&bytes) != sha256 { + return refusal(StatusCode::NOT_FOUND, "not_found"); + } + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, content_type) + .header("x-content-type-options", "nosniff") + .header( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"{sha256}\""), + ) + .header( + "content-security-policy", + "sandbox; default-src 'none'; base-uri 'none'; form-action 'none'", + ) + .header(header::CACHE_CONTROL, "public, max-age=300") + .body(EdgeBody::Once(bytes)) + .expect("should build a public source response") + } +} + +/// One loopback read of `path_and_query` through the instance's own +/// public address, carrying the capability and, for a range, `If-Match`. +/// +/// A client that streams answers with the head before any body byte, so +/// the caller judges the authenticated facts first and reads at most +/// `limit` bytes; no transport bound applies, which would refuse a large +/// declared length before its head arrived. A buffered client holds at most +/// `limit` bytes: a longer answer fails the read rather than being held. +async fn loopback_read( + services: &RuntimeServices, + preparation: &PreparationConfig, + path_and_query: &str, + capability: &str, + range: Option<&(u64, u64, String)>, + limit: usize, +) -> Result, &'static str> { + let backend = ensure_integration_backend_with_timeout( + services, + &preparation.loopback_url, + INTEGRATION_ID, + LOOPBACK_TIMEOUT, + ) + .map_err(|_| "loopback_backend")?; + let mut builder = Request::builder() + .method(Method::GET) + .uri(format!( + "{}{path_and_query}", + preparation.loopback_url.trim_end_matches('/') + )) + .header(header::ACCEPT_ENCODING, "identity") + .header(CAPABILITY_HEADER, capability); + if let Some((first, last, if_match)) = range { + builder = builder + .header(header::RANGE, format!("bytes={first}-{last}")) + .header(header::IF_MATCH, if_match.as_str()); + } + let request = builder + .body(EdgeBody::empty()) + .map_err(|_| "request_build")?; + let client = services.http_client(); + let read = PlatformHttpRequest::new(request, backend).with_cache_bypass(); + let read = if client.supports_streaming_responses() { + read.with_stream_response() + } else { + read.with_max_response_body_bytes(limit) + }; + let answer = client.send(read).await.map_err(|_| "loopback_failed")?; + Ok(answer.response) +} + +/// Refuses a pinned read whose origin facts do not admit it as the source +/// `request` names: one reader's answer, an encoded body, provenance the +/// origin already carries, a body the origin forbade transforming, another +/// origin host, another configured source than `origin_identity_sha256` +/// or another media type. A read the module did not observe reports no +/// facts and is refused as well. +fn admit_pinned_read( + facts: Option, + content_type: Option<&str>, + request: &pipeline::PrepareRequest, + origin_identity_sha256: &str, +) -> Result<(), Failure> { + let Some(origin) = facts else { + return Err(Failure::new(FailureClass::Integrity, "snapshot_unobserved")); + }; + if origin.private { + return Err(Failure::new(FailureClass::Privacy, "source_private")); + } + if origin.manifest_link { + return Err(Failure::new( + FailureClass::ExistingProvenance, + "source_has_provenance_link", + )); + } + if origin.no_transform { + return Err(Failure::new(FailureClass::Unsupported, "no_transform")); + } + if !origin.content_encoding.is_empty() { + return Err(Failure::new(FailureClass::Unsupported, "content_encoded")); + } + if origin.origin_host != request.source.origin_host { + return Err(Failure::new( + FailureClass::Integrity, + "source_origin_mismatch", + )); + } + // The same host can serve another configured source, such as a remapped + // folder, under equal per-resource validators. + if !is_digest(&origin.origin_identity_sha256) + || origin.origin_identity_sha256 != origin_identity_sha256 + { + return Err(Failure::new( + FailureClass::Integrity, + "source_identity_mismatch", + )); + } + if content_type.map(canonical_media_type).as_deref() != Some(request.source.mime.as_str()) { + return Err(Failure::new( + FailureClass::Integrity, + "source_type_mismatch", + )); + } + Ok(()) +} + +/// The version `etag` names at `path`, read in pinned ranges through the +/// reader pipeline. Every answer must be exactly its range of exactly that +/// version and length, observed by the module as a snapshot read, and +/// admitted by its origin facts as `request`'s source from the configured +/// source `origin_identity_sha256` names. +struct PinnedVersion<'a> { + services: &'a RuntimeServices, + preparation: &'a PreparationConfig, + request: &'a pipeline::PrepareRequest, + path: &'a str, + etag: &'a str, + origin_identity_sha256: &'a str, +} + +impl<'a> PinnedVersion<'a> { + /// The version of `request`'s source, refused where it names no bytes. + fn new( + services: &'a RuntimeServices, + preparation: &'a PreparationConfig, + request: &'a pipeline::PrepareRequest, + path: &'a str, + etag: &'a str, + origin_identity_sha256: &'a str, + ) -> Result { + if request.source.byte_length == 0 { + return Err(Failure::new(FailureClass::Integrity, "source_empty")); + } + Ok(Self { + services, + preparation, + request, + path, + etag, + origin_identity_sha256, + }) + } + + fn length(&self) -> u64 { + self.request.source.byte_length + } + + /// The bytes `first..=last`, at most [`MAX_RANGE_BYTES`] of them. + async fn range(&self, first: u64, last: u64) -> Result { + let length = self.length(); + if first > last || last >= length || last - first >= MAX_RANGE_BYTES { + return Err(Failure::new(FailureClass::Integrity, "read_out_of_bounds")); + } + let (path, etag) = (self.path, self.etag); + let secret = self.preparation.publication_secret.expose(); + let nonce = + sha256_hex(format!("{path}\n{etag}\n{first}\n{last}\n{}", unix_now()).as_bytes()); + let nonce = &nonce[..32]; + let capability = snapshot_capability( + secret, + path, + nonce, + unix_now() + super::protocol::CAPABILITY_LIFETIME_SECONDS, + ); + let range = (first, last, etag.to_owned()); + let expected = usize::try_from(last - first + 1).unwrap_or(usize::MAX); + let answer = loopback_read( + self.services, + self.preparation, + path, + &capability, + Some(&range), + expected, + ) + .await + .map_err(|code| Failure::new(FailureClass::Transient, code))?; + let facts = answer + .headers() + .get(FACTS_HEADER) + .and_then(|value| value.to_str().ok()) + .and_then(|value| verify_facts_header(secret, nonce, value)); + admit_pinned_read( + facts, + answer + .headers() + .get(header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()), + self.request, + self.origin_identity_sha256, + )?; + if single_strong_etag(answer.headers()).as_deref() != Some(etag) { + return Err(Failure::new( + FailureClass::Integrity, + "source_version_changed", + )); + } + if pinned_range_length(answer.status(), answer.headers(), first, last) != Some(length) { + return Err(Failure::new( + FailureClass::Integrity, + "source_range_invalid", + )); + } + // Read only once the facts, version and range admit the answer, and + // never past the range: a longer body is refused as soon as it is, a + // failed read stays a transient failure. + let bytes = match answer.into_body().into_bytes_bounded(expected).await { + Ok(bytes) => bytes, + Err(EdgeError::BadRequest { .. }) => { + return Err(Failure::new( + FailureClass::Integrity, + "source_range_invalid", + )); + } + Err(_) => return Err(Failure::new(FailureClass::Transient, "loopback_failed")), + }; + if bytes.len() != expected { + return Err(Failure::new( + FailureClass::Integrity, + "source_range_invalid", + )); + } + Ok(bytes) + } + + /// SHA-256 of every byte of the version, read in ranges of at most + /// [`MAX_RANGE_BYTES`]. + async fn digest(&self) -> Result { + let length = self.length(); + let mut hasher = sha2::Sha256::new(); + let mut first = 0_u64; + while first < length { + let last = (first + MAX_RANGE_BYTES).min(length) - 1; + hasher.update(&self.range(first, last).await?); + first = last + 1; + } + Ok(hex_lower(&hasher.finalize())) + } +} + +/// A [`PinnedVersion`] as the probe reads it, at offsets. A read inside one +/// aligned block of [`PINNED_BLOCK_BYTES`] is answered from that block, +/// read once and kept until a read needs another, so a structure walk's +/// many small reads cost one range read per block rather than one each. +/// Any other read is read exactly, in ranges of at most [`MAX_RANGE_BYTES`]. +/// The probe's own read limits bound what it may ask for in all. +struct PinnedSource<'a> { + version: &'a PinnedVersion<'a>, + block: RefCell>, +} + +impl<'a> PinnedSource<'a> { + fn new(version: &'a PinnedVersion<'a>) -> Self { + Self { + version, + block: RefCell::new(None), + } + } + + /// The aligned block starting at `start`, read once. + async fn block(&self, start: u64) -> Result { + let kept = self + .block + .borrow() + .as_ref() + .filter(|(kept_start, _)| *kept_start == start) + .map(|(_, bytes)| bytes.clone()); + if let Some(bytes) = kept { + return Ok(bytes); + } + let last = (start + PINNED_BLOCK_BYTES).min(self.version.length()) - 1; + let bytes = self.version.range(start, last).await?; + *self.block.borrow_mut() = Some((start, bytes.clone())); + Ok(bytes) + } +} + +impl SourceReader for PinnedSource<'_> { + fn size(&self) -> u64 { + self.version.length() + } + + async fn read_exact_at(&self, offset: u64, destination: &mut [u8]) -> Result<(), Failure> { + let out_of_bounds = || Failure::new(FailureClass::Integrity, "read_out_of_bounds"); + let wanted = u64::try_from(destination.len()).map_err(|_| out_of_bounds())?; + let end = offset + .checked_add(wanted) + .filter(|end| *end <= self.size()) + .ok_or_else(out_of_bounds)?; + if wanted == 0 { + return Ok(()); + } + let start = offset - offset % PINNED_BLOCK_BYTES; + if end <= start + PINNED_BLOCK_BYTES { + let block = self.block(start).await?; + let at = usize::try_from(offset - start).map_err(|_| out_of_bounds())?; + destination.copy_from_slice(&block[at..at + destination.len()]); + return Ok(()); + } + let mut first = offset; + let part_length = usize::try_from(MAX_RANGE_BYTES).unwrap_or(usize::MAX); + for part in destination.chunks_mut(part_length) { + let last = first + u64::try_from(part.len()).map_err(|_| out_of_bounds())? - 1; + part.copy_from_slice(&self.version.range(first, last).await?); + first = last + 1; + } + Ok(()) + } +} + +/// The single strong entity tag of an answer, where it has exactly one. +fn single_strong_etag(headers: &http::HeaderMap) -> Option { + let mut tags = headers.get_all(header::ETAG).iter(); + let (Some(tag), None) = (tags.next(), tags.next()) else { + return None; + }; + let tag = tag.to_str().ok()?.trim(); + (tag.starts_with('"') && tag.ends_with('"') && tag.len() >= 3).then(|| tag.to_owned()) +} + +/// Accepts one exact partial range, or an origin's exact whole-body fallback. +fn pinned_range_length( + status: StatusCode, + headers: &http::HeaderMap, + first: u64, + last: u64, +) -> Option { + let decimal = |value: &str| -> Option { + if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + value.parse().ok() + }; + if status == StatusCode::PARTIAL_CONTENT { + let mut values = headers.get_all(header::CONTENT_RANGE).iter(); + let (Some(value), None) = (values.next(), values.next()) else { + return None; + }; + let value = value.to_str().ok()?.trim().strip_prefix("bytes ")?; + let (span, total) = value.split_once('/')?; + let (start, end) = span.split_once('-')?; + let total = decimal(total)?; + (decimal(start)? == first && decimal(end)? == last && first <= last && last < total) + .then_some(total) + } else if status == StatusCode::OK && first == 0 && !headers.contains_key(header::CONTENT_RANGE) + { + let mut values = headers.get_all(header::CONTENT_LENGTH).iter(); + let (Some(value), None) = (values.next(), values.next()) else { + return None; + }; + let total = decimal(value.to_str().ok()?.trim())?; + (last.checked_add(1) == Some(total)).then_some(total) + } else { + None + } +} + +#[async_trait(?Send)] +impl IntegrationProxy for PreparationRoutes { + fn integration_name(&self) -> &'static str { + INTEGRATION_ID + } + + fn routes(&self) -> Vec { + vec![ + IntegrationEndpoint::post(SNAPSHOT_PATH), + IntegrationEndpoint::put(format!("{RESULTS_PREFIX}*")), + IntegrationEndpoint::put(format!("{MARKERS_PREFIX}*")), + IntegrationEndpoint::get(format!("{PUBLIC_SOURCE_PREFIX}*")), + ] + } + + async fn handle( + &self, + call: ModuleCall<'_>, + req: Request, + ) -> Result, Report> { + call.inject_with(self, req, Self::route)?.await + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/provenance/encypher/src/preparation/edge/tests.rs b/crates/provenance/encypher/src/preparation/edge/tests.rs new file mode 100644 index 0000000000..85850bd2a6 --- /dev/null +++ b/crates/provenance/encypher/src/preparation/edge/tests.rs @@ -0,0 +1,2447 @@ +//! The delivery instance's preparation routes, driven through their real +//! router with a scripted transport only at the network boundary: the +//! loopback reader pipeline, the Encypher API and the preparation worker. +//! +//! Every refusal here is one a forged or stale publication must meet before +//! anything is stored, and most must meet it before the API is asked +//! anything. The real probe cases run the fresh probe build the parent +//! provides, signed with a test release key, against receipts the candidate +//! API produced; see [`real_probe_receipts_publish_only_the_exact_output`]. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use edgezero_core::key_value_store::KvPage; +use futures::executor::block_on; +use http::HeaderMap; +use serde_json::Value; +use trusted_server_core::html_processor::test_support::page_settings; +use trusted_server_core::platform::test_support::build_services_with_http_client; +use trusted_server_core::platform::{ + PlatformError, PlatformHttpClient, PlatformKvStore, PlatformPendingRequest, PlatformResponse, + PlatformSelectResult, +}; + +use super::*; +use crate::preparation::pipeline::{ + PREPARED_PROTOCOL, PlannedMedia, PrepareRequest, PreparedSourceRef, ProbeRelease, + ProbeSelection, StructureChunk, TextCacheScope, TextSignRequest, +}; + +const TOKEN: &str = "cwk_test_worker_token_value"; +const POLICY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const OTHER_POLICY: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const PUBLICATION: &str = "publication-secret-for-tests"; +const SIGNAL: &str = "signal-secret-for-tests"; +const HOST: &str = "publisher.example.com"; +const PLAN_ID: &str = "0f8fad5b-d9cb-469f-a165-70867728950e"; +const NONCE: &str = "0123456789abcdef0123456789abcdef"; +const MARKER: &str = "\u{E0100}\u{E0101}\u{FE01}"; +const SOURCE: &[u8] = b"\x89PNG\r\n\x1a\n-source-pixels"; +const OTHER_SOURCE: &[u8] = b"\x89PNG\r\n\x1a\n-other-pixels!"; +const ETAG: &str = "\"v1\""; +/// Core's configured-origin identity of the media route a large source is +/// published from, and of the same route after an operator remapped it to +/// another folder of the same origin host. +const CONFIGURED_SOURCE: &str = "https://publisher.example.com/library-a\0/media/"; +const REMAPPED_SOURCE: &str = "https://publisher.example.com/library-b\0/media/"; + +// --- doubles at the network and storage boundaries ------------------------ + +/// One request the scripted transport received. +#[derive(Debug, Clone)] +struct Seen { + method: Method, + uri: String, + headers: HeaderMap, + body: Bytes, +} + +impl Seen { + fn header(&self, name: &str) -> Option<&str> { + self.headers.get(name).and_then(|value| value.to_str().ok()) + } +} + +type Responder = Box Response + Send + Sync>; + +/// A transport that answers each request from what it asked and records +/// every request. +struct ScriptedHttp { + responder: Responder, + seen: Mutex>, +} + +impl ScriptedHttp { + fn new(responder: impl Fn(&Seen) -> Response + Send + Sync + 'static) -> Arc { + Arc::new(Self { + responder: Box::new(responder), + seen: Mutex::new(Vec::new()), + }) + } + + /// A transport that refuses everything, for routes that must refuse + /// before reaching any network. + fn refusing() -> Arc { + Self::new(|_| answer(500, Vec::new(), &[])) + } + + fn seen(&self) -> Vec { + self.seen.lock().expect("should lock requests").clone() + } + + fn api_calls(&self) -> Vec { + self.seen() + .into_iter() + .filter(|seen| seen.uri.starts_with("https://api.example.com")) + .collect() + } +} + +#[async_trait(?Send)] +impl PlatformHttpClient for ScriptedHttp { + async fn send( + &self, + request: PlatformHttpRequest, + ) -> Result> { + let (parts, body) = request.request.into_parts(); + let body = body + .into_bytes_bounded(64 * 1024 * 1024) + .await + .expect("should read the request body"); + let seen = Seen { + method: parts.method, + uri: parts.uri.to_string(), + headers: parts.headers, + body, + }; + let response = (self.responder)(&seen); + self.seen.lock().expect("should lock requests").push(seen); + Ok(PlatformResponse::new(response)) + } + + async fn send_async( + &self, + _request: PlatformHttpRequest, + ) -> Result> { + Err(Report::new(PlatformError::HttpClient)) + } + + async fn select( + &self, + _pending_requests: Vec, + ) -> Result> { + Err(Report::new(PlatformError::HttpClient)) + } +} + +/// An HTTP answer of `status` with `body` and `headers`. +fn answer(status: u16, body: Vec, headers: &[(&str, String)]) -> Response { + let mut builder = Response::builder().status(status); + for (name, value) in headers { + builder = builder.header(*name, value.as_str()); + } + builder + .body(EdgeBody::from(body)) + .expect("should build an answer") +} + +/// A runtime KV store held in memory, whose reads of keys starting with +/// `failing_prefix` fail. +#[derive(Default)] +struct MemoryKv { + entries: Mutex>, + failing_prefix: Option<&'static str>, +} + +impl MemoryKv { + fn failing(prefix: &'static str) -> Self { + Self { + failing_prefix: Some(prefix), + ..Self::default() + } + } + + fn get(&self, key: &str) -> Option { + self.entries.lock().expect("should lock").get(key).cloned() + } + + fn put(&self, key: &str, value: &impl serde::Serialize) { + self.entries.lock().expect("should lock").insert( + key.to_owned(), + Bytes::from(serde_json::to_vec(value).expect("should encode")), + ); + } +} + +#[async_trait(?Send)] +impl PlatformKvStore for MemoryKv { + async fn get_bytes(&self, key: &str) -> Result, KvError> { + if self + .failing_prefix + .is_some_and(|prefix| key.starts_with(prefix)) + { + return Err(KvError::Unavailable); + } + Ok(self.get(key)) + } + + async fn put_bytes(&self, key: &str, value: Bytes) -> Result<(), KvError> { + self.entries + .lock() + .expect("should lock") + .insert(key.to_owned(), value); + Ok(()) + } + + async fn put_bytes_with_ttl( + &self, + key: &str, + value: Bytes, + _ttl: Duration, + ) -> Result<(), KvError> { + self.put_bytes(key, value).await + } + + async fn delete(&self, key: &str) -> Result<(), KvError> { + self.entries.lock().expect("should lock").remove(key); + Ok(()) + } + + async fn list_keys_page( + &self, + _prefix: &str, + _cursor: Option<&str>, + _limit: usize, + ) -> Result { + Ok(KvPage::default()) + } +} + +// --- the deployment under test -------------------------------------------- + +fn config_value(domain: &str, public_host: &str, routes: &[&str]) -> Value { + json!({ + "api_base_url": "https://api.example.com", + "domain": domain, + "worker_token": TOKEN, + "max_large_media_bytes": 1_073_741_824_u64, + "execution": "async_prepared", + "preparation": { + "worker_url": "https://prep.publisher.example.com", + "loopback_url": format!("https://{public_host}"), + "deployment_id": "test", + "public_host": public_host, + "signal_secret": SIGNAL, + "publication_secret": PUBLICATION, + "routes": routes, + "variants": [{"name": "edition", "values": ["uk", "us"]}], + }, + }) +} + +fn routes_for(config: Value) -> PreparationRoutes { + let config: EncypherConfig = serde_json::from_value(config).expect("should parse config"); + PreparationRoutes::new(Arc::new(Signer::new(config, 5_000_000))) +} + +fn routes() -> PreparationRoutes { + routes_for(config_value(HOST, HOST, &["/news/", "/media/"])) +} + +fn services(http: &Arc, kv: &Arc) -> RuntimeServices { + build_services_with_http_client(Arc::clone(http) as Arc) + .with_kv_store(Arc::clone(kv) as Arc) +} + +fn call( + routes: &PreparationRoutes, + services: &RuntimeServices, + method: Method, + path: &str, + token: Option<&str>, + body: Vec, +) -> Response { + let mut builder = Request::builder() + .method(method) + .uri(format!("https://{HOST}{path}")); + if let Some(token) = token { + builder = builder.header(TOKEN_HEADER, token); + } + let request = builder + .body(EdgeBody::from(body)) + .expect("should build the request"); + block_on(routes.route(request, services)).expect("should answer") +} + +fn publish( + routes: &PreparationRoutes, + services: &RuntimeServices, + token: &str, + path_identity: &str, + envelope: &ResultEnvelope, +) -> Response { + call( + routes, + services, + Method::PUT, + &format!("{RESULTS_PREFIX}{path_identity}"), + Some(token), + serde_json::to_vec(envelope).expect("should encode the envelope"), + ) +} + +fn body_of(response: Response) -> Bytes { + block_on(response.into_body().into_bytes_bounded(64 * 1024 * 1024)) + .expect("should read the body") +} + +/// The refusal code of `response`, after checking its status. +fn refused_with(response: Response, status: StatusCode) -> String { + assert_eq!(response.status(), status, "should answer {status}"); + let body: Value = serde_json::from_slice(&body_of(response)).expect("should answer JSON"); + body["error"]["code"] + .as_str() + .expect("should name a code") + .to_owned() +} + +// --- envelopes ------------------------------------------------------------ + +fn page_url(path: &str) -> String { + format!("https://{HOST}{path}") +} + +fn media_request(carrier: Carrier, source: &[u8], mime: &str, path: &str) -> PrepareRequest { + PrepareRequest { + contract_version: 2, + carrier_requirement: match carrier { + Carrier::External => "external_allowed", + Carrier::Embedded => "embedded_required", + } + .to_owned(), + page_url: page_url(path), + expected_policy_digest: POLICY.to_owned(), + protocol: PREPARED_PROTOCOL.to_owned(), + probe: ProbeSelection { + sha256: "e".repeat(64), + planner_id: "d".repeat(64), + }, + source: PreparedSourceRef { + reference: page_url(path), + origin_host: HOST.to_owned(), + mime: mime.to_owned(), + sha256: sha256_hex(source), + byte_length: u64::try_from(source.len()).expect("should fit"), + reference_kind: "origin".to_owned(), + }, + structure: vec![StructureChunk { + offset: 0, + b64: "AAAA".to_owned(), + }], + } +} + +fn planned(request: PrepareRequest) -> PlannedMedia { + PlannedMedia { + digest: request.source.sha256.clone(), + request, + release: ProbeRelease { + abi: 1, + planner_id: "d".repeat(64), + release: 3, + sha256: "e".repeat(64), + signature: "AAAA".to_owned(), + }, + plan_id: PLAN_ID.to_owned(), + geometry: json!({"disposition": "external"}), + idempotency_key: "f".repeat(64), + } +} + +fn media_identity(routes: &PreparationRoutes, request: &PrepareRequest, variant: &str) -> String { + let carrier = request.carrier(); + let mime = match carrier { + Carrier::External => canonical_media_type(&request.source.mime), + Carrier::Embedded => request.source.mime.clone(), + }; + MediaIdentity { + credential_context: routes.signer.credential_context(), + policy_digest: &request.expected_policy_digest, + page_url: &request.page_url, + variant, + carrier, + mime: &mime, + source_sha256: &request.source.sha256, + } + .digest() +} + +fn envelope(identity: &str, item: EnvelopeItem) -> ResultEnvelope { + ResultEnvelope { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: "test".to_owned(), + identity: identity.to_owned(), + retirement_epoch: 0, + variant: String::new(), + item, + } +} + +fn media_envelope(routes: &PreparationRoutes) -> (String, ResultEnvelope) { + let request = media_request(Carrier::External, SOURCE, "image/png", "/media/a.png"); + let identity = media_identity(routes, &request, ""); + let item = EnvelopeItem::Media { + planned: planned(request), + source_b64: STANDARD.encode(SOURCE), + }; + (identity.clone(), envelope(&identity, item)) +} + +fn text_request(text: &str) -> TextSignRequest { + TextSignRequest { + text: text.to_owned(), + page_url: page_url("/news/a"), + document_title: Some("Harbor".to_owned()), + boundary_selector: "article".to_owned(), + expected_policy_digest: POLICY.to_owned(), + cache_scope: TextCacheScope::ExactRequest, + } +} + +fn text_envelope(routes: &PreparationRoutes, request: TextSignRequest) -> (String, ResultEnvelope) { + let identity = TextIdentity { + credential_context: routes.signer.credential_context(), + policy_digest: &request.expected_policy_digest, + page_url: &request.page_url, + variant: "", + text: &request.text, + document_title: request.document_title.as_deref(), + boundary_selector: &request.boundary_selector, + } + .digest(); + let item = EnvelopeItem::Text { + request, + idempotency_key: "9".repeat(64), + }; + (identity.clone(), envelope(&identity, item)) +} + +fn sign_answer(policy: &str) -> Vec { + serde_json::to_vec(&json!({ + "success": true, + "policy_digest": policy, + "embedding_plan": { + "index_unit": "codepoint", + "operations": [{"insert_after_index": 5, "marker": MARKER}], + }, + "manifest_url": "https://api.example.com/manifests/doc-1", + "verification_url": "https://api.example.com/verify/doc-1", + "document_id": "doc-1", + })) + .expect("should encode the sign answer") +} + +const ARTICLE_TEXT: &str = "Harbor council votes on the ferry schedule for the coming season."; + +// --- authentication, deployment and identity ------------------------------ + +#[test] +fn publication_needs_the_publication_scope_this_deployment_and_its_own_identity() { + let routes = routes(); + let http = ScriptedHttp::refusing(); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let (identity, genuine) = media_envelope(&routes); + + let signal_scope = publish(&routes, &services, SIGNAL, &identity, &genuine); + assert_eq!( + refused_with(signal_scope, StatusCode::UNAUTHORIZED), + "unauthorized", + "the reader-signal secret should not publish" + ); + + let mut other_deployment = genuine.clone(); + other_deployment.deployment_id = "other".to_owned(); + assert_eq!( + refused_with( + publish( + &routes, + &services, + PUBLICATION, + &identity, + &other_deployment + ), + StatusCode::FORBIDDEN + ), + "wrong_deployment" + ); + + let elsewhere = "1".repeat(64); + assert_eq!( + refused_with( + publish(&routes, &services, PUBLICATION, &elsewhere, &genuine), + StatusCode::FORBIDDEN + ), + "wrong_deployment", + "an envelope should publish only under the identity it names" + ); + + let mut unlisted = genuine.clone(); + unlisted.variant = "edition=fr".to_owned(); + assert_eq!( + refused_with( + publish(&routes, &services, PUBLICATION, &identity, &unlisted), + StatusCode::FORBIDDEN + ), + "variant_not_admitted" + ); + + assert!(http.seen().is_empty(), "nothing should reach any network"); + assert!( + kv.get(&result_key(&identity)).is_none(), + "nothing should be stored" + ); +} + +#[test] +fn a_retired_or_unreadable_marker_refuses_publication_before_the_api_is_asked() { + let routes = routes(); + let (identity, genuine) = media_envelope(&routes); + + let http = ScriptedHttp::refusing(); + let kv = Arc::new(MemoryKv::default()); + kv.put(&marker_key(&identity), &RetirementMarker { epoch: 2 }); + let retired = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &genuine, + ); + assert_eq!(refused_with(retired, StatusCode::CONFLICT), "retired"); + assert!(kv.get(&result_key(&identity)).is_none()); + + let unreadable = Arc::new(MemoryKv::failing("encypher:prep:v1:retired:")); + let refused = publish( + &routes, + &services(&http, &unreadable), + PUBLICATION, + &identity, + &genuine, + ); + assert_eq!( + refused_with(refused, StatusCode::SERVICE_UNAVAILABLE), + "marker_unavailable", + "a marker that cannot be read should refuse, never publish" + ); + assert!(unreadable.get(&result_key(&identity)).is_none()); + assert!(http.seen().is_empty(), "the API should never be asked"); +} + +#[test] +fn worker_supplied_identities_sources_and_carriers_are_never_taken_on_trust() { + let routes = routes(); + let http = ScriptedHttp::refusing(); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + + // An identity for one source over a request naming another. + let request = media_request(Carrier::External, SOURCE, "image/png", "/media/a.png"); + let other = media_request(Carrier::External, OTHER_SOURCE, "image/png", "/media/a.png"); + let wrong_identity = media_identity(&routes, &other, ""); + let substituted = envelope( + &wrong_identity, + EnvelopeItem::Media { + planned: planned(request.clone()), + source_b64: STANDARD.encode(SOURCE), + }, + ); + assert_eq!( + refused_with( + publish( + &routes, + &services, + PUBLICATION, + &wrong_identity, + &substituted + ), + StatusCode::UNPROCESSABLE_ENTITY + ), + "identity_mismatch" + ); + + // Self-consistent identity and request, but bytes one byte off. + let identity = media_identity(&routes, &request, ""); + let mut tampered = SOURCE.to_vec(); + *tampered.last_mut().expect("should have bytes") ^= 1; + let one_byte = envelope( + &identity, + EnvelopeItem::Media { + planned: planned(request.clone()), + source_b64: STANDARD.encode(&tampered), + }, + ); + assert_eq!( + refused_with( + publish(&routes, &services, PUBLICATION, &identity, &one_byte), + StatusCode::UNPROCESSABLE_ENTITY + ), + "source_digest_mismatch" + ); + + // An embedded item whose source bytes are not the ones it names. + let embedded = media_request(Carrier::Embedded, SOURCE, "image/png", "/news/a"); + let embedded_identity = media_identity(&routes, &embedded, ""); + let embedded_forgery = envelope( + &embedded_identity, + EnvelopeItem::Embedded { + planned: planned(embedded.clone()), + source_b64: STANDARD.encode(OTHER_SOURCE), + output_b64: STANDARD.encode(OTHER_SOURCE), + }, + ); + assert_eq!( + refused_with( + publish( + &routes, + &services, + PUBLICATION, + &embedded_identity, + &embedded_forgery + ), + StatusCode::UNPROCESSABLE_ENTITY + ), + "source_digest_mismatch" + ); + + // An external plan presented as embedded, and the reverse. + let as_embedded = envelope( + &identity, + EnvelopeItem::Embedded { + planned: planned(request.clone()), + source_b64: STANDARD.encode(SOURCE), + output_b64: STANDARD.encode(SOURCE), + }, + ); + assert_eq!( + refused_with( + publish(&routes, &services, PUBLICATION, &identity, &as_embedded), + StatusCode::UNPROCESSABLE_ENTITY + ), + "carrier_mismatch" + ); + let as_external = envelope( + &embedded_identity, + EnvelopeItem::Media { + planned: planned(embedded), + source_b64: STANDARD.encode(SOURCE), + }, + ); + assert_eq!( + refused_with( + publish( + &routes, + &services, + PUBLICATION, + &embedded_identity, + &as_external + ), + StatusCode::UNPROCESSABLE_ENTITY + ), + "carrier_mismatch" + ); + + // Addresses outside the claimed host or the configured routes. + for path_or_host in [ + "https://evil.example.net/media/a.png", + "https://publisher.example.com/private/a.png", + ] { + let mut outside = request.clone(); + outside.page_url = path_or_host.to_owned(); + let outside_identity = media_identity(&routes, &outside, ""); + let item = envelope( + &outside_identity, + EnvelopeItem::Media { + planned: planned(outside), + source_b64: STANDARD.encode(SOURCE), + }, + ); + assert_eq!( + refused_with( + publish(&routes, &services, PUBLICATION, &outside_identity, &item), + StatusCode::UNPROCESSABLE_ENTITY + ), + "page_url_outside_deployment", + "{path_or_host} should be refused" + ); + } + + assert!( + http.seen().is_empty(), + "every forgery should be refused before the API is asked" + ); + assert!( + kv.entries.lock().expect("should lock").is_empty(), + "nothing should be stored" + ); +} + +// --- text: recovered, never signed ---------------------------------------- + +#[test] +fn text_publication_refuses_missing_or_unknown_cache_scope_before_recovery() { + let routes = routes(); + let http = ScriptedHttp::refusing(); + let kv = Arc::new(MemoryKv::default()); + let (identity, genuine) = text_envelope(&routes, text_request(ARTICLE_TEXT)); + for scope in [None, Some(json!("legacy")), Some(Value::Null)] { + let mut body = serde_json::to_value(&genuine).expect("should encode an envelope"); + let request = body["item"]["request"] + .as_object_mut() + .expect("should carry a request"); + if let Some(scope) = scope { + request.insert("cache_scope".to_owned(), scope); + } else { + request.remove("cache_scope"); + } + let response = call( + &routes, + &services(&http, &kv), + Method::PUT, + &format!("{RESULTS_PREFIX}{identity}"), + Some(PUBLICATION), + serde_json::to_vec(&body).expect("should encode an unsupported request"), + ); + assert_eq!( + refused_with(response, StatusCode::BAD_REQUEST), + "malformed_request" + ); + } + assert!( + http.seen().is_empty(), + "unsupported scope must never reach recovery" + ); + assert!(kv.entries.lock().expect("should lock").is_empty()); +} + +#[test] +fn text_publication_refuses_old_cache_identity_and_substituted_title_or_url() { + let routes = routes(); + let http = ScriptedHttp::refusing(); + let kv = Arc::new(MemoryKv::default()); + let request = text_request(ARTICLE_TEXT); + let (identity, genuine) = text_envelope(&routes, request.clone()); + let text_hash = crate::media_api::sha256_hex(request.text.as_bytes()); + let old_identity = super::super::protocol::domain_digest( + "encypher-preparation/1:text", + &[ + routes.signer.credential_context().as_bytes(), + request.expected_policy_digest.as_bytes(), + request.page_url.as_bytes(), + b"", + super::super::protocol::TEXT_PROJECTION_VERSION.as_bytes(), + text_hash.as_bytes(), + b"1", + b"Harbor", + request.boundary_selector.as_bytes(), + ], + ); + let mut old = genuine.clone(); + old.identity = old_identity.clone(); + assert_eq!( + refused_with( + publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &old_identity, + &old + ), + StatusCode::UNPROCESSABLE_ENTITY, + ), + "identity_mismatch", + "even an exact-scoped envelope must not publish under the former cache domain" + ); + for changed_request in [ + TextSignRequest { + document_title: Some("Revised harbor title".to_owned()), + ..request.clone() + }, + TextSignRequest { + page_url: page_url("/news/mirror"), + ..request + }, + ] { + let (_, mut changed) = text_envelope(&routes, changed_request); + changed.identity = identity.clone(); + assert_eq!( + refused_with( + publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &changed + ), + StatusCode::UNPROCESSABLE_ENTITY, + ), + "identity_mismatch", + "title and URL cannot change under an existing prepared identity" + ); + } + assert!( + http.seen().is_empty(), + "stale identities must never reach recovery" + ); + assert!(kv.entries.lock().expect("should lock").is_empty()); +} +#[test] +fn a_text_result_is_recovered_never_signed_and_published_under_its_identity() { + let routes = routes(); + let http = ScriptedHttp::new(|seen| { + if seen.uri == "https://api.example.com/api/v1/public/cdn/sign/recover" { + answer(200, sign_answer(POLICY), &[]) + } else { + answer(500, Vec::new(), &[]) + } + }); + let kv = Arc::new(MemoryKv::default()); + let request = text_request(ARTICLE_TEXT); + let (identity, genuine) = text_envelope(&routes, request.clone()); + + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &genuine, + ); + + assert_eq!(response.status(), StatusCode::OK, "should publish"); + let calls = http.seen(); + assert_eq!(calls.len(), 1, "should make exactly one API call"); + let call = &calls[0]; + assert_eq!(call.method, Method::POST); + assert_eq!( + call.uri, "https://api.example.com/api/v1/public/cdn/sign/recover", + "should recover, never call the sign route" + ); + assert_eq!( + call.header("idempotency-key"), + Some("9".repeat(64).as_str()) + ); + assert_eq!(call.header("x-encypher-worker-token"), Some(TOKEN)); + let recovered: Value = serde_json::from_slice(&call.body).expect("should carry JSON"); + assert_eq!(recovered["cache_scope"], "exact_request"); + assert_eq!( + serde_json::from_slice::(&call.body).expect("should send the request"), + request, + "should recover exactly the frozen request" + ); + let record: PublishedRecord = serde_json::from_slice( + &kv.get(&result_key(&identity)) + .expect("should store the record"), + ) + .expect("should store a record"); + assert_eq!(record.identity, identity); + assert!( + matches!(record.result, PublishedResult::Text { scalars, .. } if scalars == ARTICLE_TEXT.chars().count()), + "should store the recovered plan for this exact text" + ); + + // A worker that changes the text under the same identity is refused. + let (_, mut changed) = text_envelope( + &routes, + text_request("A different article entirely, long enough."), + ); + changed.identity = identity.clone(); + assert_eq!( + refused_with( + publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &changed + ), + StatusCode::UNPROCESSABLE_ENTITY + ), + "identity_mismatch" + ); +} + +#[test] +fn a_text_result_the_api_no_longer_holds_or_holds_under_another_policy_is_refused() { + let routes = routes(); + let (identity, genuine) = text_envelope(&routes, text_request(ARTICLE_TEXT)); + for (status, body, expected_status, code) in [ + (404, Vec::new(), StatusCode::GONE, "result_not_retained"), + ( + 200, + sign_answer(OTHER_POLICY), + StatusCode::CONFLICT, + "policy_digest_mismatch", + ), + ] { + let http = ScriptedHttp::new(move |_| answer(status, body.clone(), &[])); + let kv = Arc::new(MemoryKv::default()); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &genuine, + ); + assert_eq!(refused_with(response, expected_status), code); + assert!( + kv.get(&result_key(&identity)).is_none(), + "{code}: nothing stored" + ); + assert!( + http.seen() + .iter() + .all(|seen| !seen.uri.ends_with("/cdn/sign")), + "{code}: should never call the sign route" + ); + } +} + +// --- retirement markers --------------------------------------------------- + +fn retire( + routes: &PreparationRoutes, + services: &RuntimeServices, + token: &str, + identity: &str, + epoch: u64, +) -> Response { + let marker = MarkerEnvelope { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: "test".to_owned(), + identity: identity.to_owned(), + epoch, + }; + call( + routes, + services, + Method::PUT, + &format!("{MARKERS_PREFIX}{identity}"), + Some(token), + serde_json::to_vec(&marker).expect("should encode the marker"), + ) +} + +fn stored_marker(kv: &MemoryKv, identity: &str) -> RetirementMarker { + serde_json::from_slice( + &kv.get(&marker_key(identity)) + .expect("should store a marker"), + ) + .expect("should store a marker") +} + +#[test] +fn retirement_markers_are_monotonic_and_refuse_late_results() { + let routes = routes(); + let http = ScriptedHttp::new(|_| answer(200, sign_answer(POLICY), &[])); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let (identity, genuine) = text_envelope(&routes, text_request(ARTICLE_TEXT)); + assert_eq!( + publish(&routes, &services, PUBLICATION, &identity, &genuine).status(), + StatusCode::OK + ); + assert!(kv.get(&result_key(&identity)).is_some()); + + assert_eq!( + refused_with( + retire(&routes, &services, SIGNAL, &identity, 3), + StatusCode::UNAUTHORIZED + ), + "unauthorized", + "the reader-signal secret should not retire" + ); + let acknowledged = retire(&routes, &services, PUBLICATION, &identity, 3); + assert_eq!(acknowledged.status(), StatusCode::OK); + let body: Value = serde_json::from_slice(&body_of(acknowledged)).expect("should answer JSON"); + assert_eq!(body["acknowledged"], json!(identity)); + assert_eq!(body["epoch"], json!(3)); + assert_eq!(stored_marker(&kv, &identity).epoch, 3); + assert!( + kv.get(&result_key(&identity)).is_none(), + "should drop the retired result" + ); + + let older = retire(&routes, &services, PUBLICATION, &identity, 1); + assert_eq!(older.status(), StatusCode::OK); + assert_eq!( + stored_marker(&kv, &identity).epoch, + 3, + "a delayed older marker should never lower the epoch" + ); + + let calls_before = http.seen().len(); + let late = publish(&routes, &services, PUBLICATION, &identity, &genuine); + assert_eq!(refused_with(late, StatusCode::CONFLICT), "retired"); + assert_eq!( + http.seen().len(), + calls_before, + "a late result should be refused without asking the API" + ); + assert!(kv.get(&result_key(&identity)).is_none()); + assert_eq!( + stored_marker(&kv, &identity).epoch, + 3, + "the marker should stand" + ); +} + +// --- large media: the full source is read again --------------------------- + +/// What the loopback reader pipeline answers for a large source. +#[derive(Clone)] +struct Origin { + bytes: Arc>, + etag: &'static str, + facts: Option, + facts_secret: &'static str, + content_type: &'static str, +} + +impl Origin { + fn honest(bytes: Vec) -> Self { + Self { + bytes: Arc::new(bytes), + etag: ETAG, + facts: Some(SnapshotOrigin { + status: 206, + origin_host: HOST.to_owned(), + origin_identity_sha256: sha256_hex(CONFIGURED_SOURCE.as_bytes()), + ..SnapshotOrigin::default() + }), + facts_secret: PUBLICATION, + content_type: "image/png", + } + } + + /// Answers a pinned range read the way the reader pipeline does, with + /// facts authenticated for the capability's nonce; anything else, the + /// API's recovery included, finds nothing. + fn responder(self) -> impl Fn(&Seen) -> Response + Send + Sync + 'static { + move |seen| { + let Some(capability) = seen.header(CAPABILITY_HEADER) else { + return answer(404, Vec::new(), &[]); + }; + let nonce = capability.split('.').nth(2).expect("should carry a nonce"); + let range = seen + .header("range") + .and_then(|value| value.strip_prefix("bytes=")) + .and_then(|value| value.split_once('-')) + .expect("should read a range"); + let first: usize = range.0.parse().expect("should parse"); + let last: usize = range.1.parse().expect("should parse"); + let mut headers = vec![ + ( + "content-range", + format!("bytes {first}-{last}/{}", self.bytes.len()), + ), + ("etag", self.etag.to_owned()), + ("content-type", self.content_type.to_owned()), + ]; + if let Some(facts) = &self.facts { + headers.push((FACTS_HEADER, facts_header(self.facts_secret, nonce, facts))); + } + answer(206, self.bytes[first..=last].to_vec(), &headers) + } + } +} + +/// The production-signed release every real-probe fixture was planned with. +fn signed_release() -> ProbeRelease { + serde_json::from_slice(include_bytes!("../../fixtures/prepared/descriptor.json")) + .expect("should parse the production-signed descriptor") +} + +/// A store already holding the signed release's verified artifact, as one +/// that loaded it before does, so a publication needs no API call to run it. +fn probe_kv() -> Arc { + let kv = Arc::new(MemoryKv::default()); + kv.entries.lock().expect("should lock").insert( + format!("{PROBE_KEY_PREFIX}:{}", signed_release().sha256), + Bytes::from(fixture("probe.wasm")), + ); + kv +} + +/// Appends one PNG chunk of `kind` holding `data`, with its CRC. +fn png_chunk(png: &mut Vec, kind: &[u8; 4], data: &[u8]) { + png.extend_from_slice(&u32::try_from(data.len()).expect("should fit").to_be_bytes()); + png.extend_from_slice(kind); + png.extend_from_slice(data); + let mut crc = !0_u32; + for byte in kind.iter().chain(data) { + crc ^= u32::from(*byte); + for _ in 0..8 { + crc = if crc & 1 == 1 { + (crc >> 1) ^ 0xEDB8_8320 + } else { + crc >> 1 + }; + } + } + png.extend_from_slice(&(!crc).to_be_bytes()); +} + +/// The retained real PNG with `chunks` inserted before its `IEND`. +fn png_with(chunks: &[(&[u8; 4], &[u8])]) -> Vec { + let base: &[u8] = include_bytes!("../../fixtures/prepared/external-png/source.png"); + let (body, end) = base.split_at(base.len() - 12); + assert_eq!(&end[4..8], b"IEND", "the fixture should end with IEND"); + let mut png = body.to_vec(); + for (kind, data) in chunks { + png_chunk(&mut png, kind, data); + } + png.extend_from_slice(end); + png +} + +/// A PNG of exactly `length` bytes: the real fixture with one private +/// chunk of sevens before its `IEND`, whose last data byte is `last`. +fn large_png(length: usize, last: u8) -> Vec { + let base = png_with(&[]).len(); + let mut data = vec![7_u8; length - base - 12]; + *data.last_mut().expect("should have data") = last; + let png = png_with(&[(b"prVt", data.as_slice())]); + assert_eq!(png.len(), length, "should be exactly the asked length"); + png +} + +/// An XMP packet naming a remote manifest with `dcterms:provenance`. +const PROVENANCE_XMP: &[u8] = br#""#; + +/// The uncompressed `iTXt` data carrying `packet` as the PNG's XMP. +fn xmp_itxt(packet: &[u8]) -> Vec { + let mut data = b"XML:com.adobe.xmp\0\0\0\0\0".to_vec(); + data.extend_from_slice(packet); + data +} + +fn large_envelope(routes: &PreparationRoutes, planned_source: &[u8]) -> (String, ResultEnvelope) { + large_envelope_bound( + routes, + planned_source, + &sha256_hex(CONFIGURED_SOURCE.as_bytes()), + ) +} + +/// A large envelope whose worker says it read the configured source +/// `origin_identity_sha256` names, under an identity bound to it. +fn large_envelope_bound( + routes: &PreparationRoutes, + planned_source: &[u8], + origin_identity_sha256: &str, +) -> (String, ResultEnvelope) { + let release = signed_release(); + let mut request = media_request( + Carrier::External, + planned_source, + "image/png", + "/media/ferry.png", + ); + request.probe = ProbeSelection { + sha256: release.sha256.clone(), + planner_id: release.planner_id.clone(), + }; + let identity = LargeLocator { + credential_context: routes.signer.credential_context(), + policy_digest: POLICY, + reader_url: &request.page_url, + variant: "", + origin_host: HOST, + origin_identity_sha256, + mime: "image/png", + etag: ETAG, + length: request.source.byte_length, + } + .digest(); + let mut planned = planned(request); + planned.release = release; + let item = EnvelopeItem::Large { + planned, + etag: ETAG.to_owned(), + origin_identity_sha256: origin_identity_sha256.to_owned(), + }; + (identity.clone(), envelope(&identity, item)) +} + +/// The pinned reads `seen` made, in order, as their `Range` headers. +fn pinned_ranges(seen: &[Seen]) -> Vec { + seen.iter() + .filter(|seen| seen.header(CAPABILITY_HEADER).is_some()) + .map(|seen| seen.header("range").unwrap_or_default().to_owned()) + .collect() +} + +/// Whether a `bytes=first-last` range is at most one pinned block. +fn within_block(range: &str) -> bool { + range + .strip_prefix("bytes=") + .and_then(|range| range.split_once('-')) + .and_then(|(first, last)| Some(last.parse::().ok()? - first.parse::().ok()?)) + .is_some_and(|span| span < PINNED_BLOCK_BYTES) +} + +#[test] +fn large_publication_hashes_every_byte_of_the_pinned_version_before_the_api_is_asked() { + let routes = routes(); + let length = usize::try_from(MAX_RANGE_BYTES).expect("should fit") + 10; + let source = large_png(length, 0); + + // The worker's claimed digest is the source's, but the version the tag + // names differs near its end, beyond the first range. + let served = large_png(length, 1); + let http = ScriptedHttp::new(Origin::honest(served).responder()); + let kv = probe_kv(); + let (identity, forged) = large_envelope(&routes, &source); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &forged, + ); + assert_eq!( + refused_with(response, StatusCode::UNPROCESSABLE_ENTITY), + "source_digest_mismatch" + ); + assert!(http.api_calls().is_empty(), "the API should never be asked"); + assert!(kv.get(&result_key(&identity)).is_none()); + + // An honest version passes the full read, and only then is the API's + // receipt asked for; here it holds none. + let http = ScriptedHttp::new(Origin::honest(source.clone()).responder()); + let kv = probe_kv(); + let (identity, genuine) = large_envelope(&routes, &source); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &genuine, + ); + assert_eq!( + refused_with(response, StatusCode::GONE), + "result_not_retained" + ); + let seen = http.seen(); + let reads: Vec<&Seen> = seen + .iter() + .filter(|seen| seen.header(CAPABILITY_HEADER).is_some()) + .collect(); + let ranges = pinned_ranges(&seen); + let (inspection, hashing) = ranges.split_at(ranges.len() - 2); + assert_eq!( + hashing, + [ + format!("bytes=0-{}", MAX_RANGE_BYTES - 1), + format!("bytes={}-{}", MAX_RANGE_BYTES, length - 1), + ], + "should read every byte in pinned ranges" + ); + assert!( + !inspection.is_empty() && inspection.iter().all(|range| within_block(range)), + "the provenance inspection should read only blocks of the version, first" + ); + for read in &reads { + assert_eq!(read.uri, format!("https://{HOST}/media/ferry.png")); + assert_eq!( + read.header("if-match"), + Some(ETAG), + "should pin the version" + ); + assert_eq!(read.header("accept-encoding"), Some("identity")); + let capability = read.header(CAPABILITY_HEADER).expect("should carry it"); + assert!( + verify_snapshot_capability(PUBLICATION, "/media/ferry.png", capability, unix_now()) + .is_some(), + "should carry a capability for exactly this path" + ); + } + let last_read = seen + .iter() + .rposition(|seen| seen.header(CAPABILITY_HEADER).is_some()) + .expect("should read"); + let recover = seen + .iter() + .position(|seen| seen.uri.ends_with("/media/hash/recover")) + .expect("should ask for the receipt"); + assert!( + recover > last_read, + "the receipt should be asked for only after the read" + ); + assert!(kv.get(&result_key(&identity)).is_none()); +} + +#[test] +fn large_publication_reads_the_admitted_variant_not_the_query_free_resource() { + let routes = routes(); + let length = usize::try_from(MAX_RANGE_BYTES).expect("should fit a range") + 10; + let source_b = large_png(length, 7); + let source_a = large_png(length, 8); + assert_eq!(source_a.len(), source_b.len()); + assert_ne!(sha256_hex(&source_a), sha256_hex(&source_b)); + let base = Origin::honest(source_a).responder(); + let variant = Origin::honest(source_b.clone()).responder(); + let http = ScriptedHttp::new(move |seen| { + if seen.uri == format!("https://{HOST}/media/ferry.png?edition=uk") { + variant(seen) + } else { + base(seen) + } + }); + let kv = probe_kv(); + let (_, mut genuine) = large_envelope(&routes, &source_b); + genuine.variant = "edition=uk".to_owned(); + let EnvelopeItem::Large { + planned, + etag, + origin_identity_sha256, + } = &genuine.item + else { + panic!("should have a large publication"); + }; + genuine.identity = LargeLocator { + credential_context: routes.signer.credential_context(), + policy_digest: POLICY, + reader_url: &planned.request.page_url, + variant: &genuine.variant, + origin_host: HOST, + origin_identity_sha256, + mime: "image/png", + etag, + length: planned.request.source.byte_length, + } + .digest(); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &genuine.identity, + &genuine, + ); + assert_eq!( + refused_with(response, StatusCode::GONE), + "result_not_retained", + "B's digest should pass before the absent API receipt refuses publication" + ); + let seen = http.seen(); + let reads: Vec<&Seen> = seen + .iter() + .filter(|seen| seen.header(CAPABILITY_HEADER).is_some()) + .collect(); + let ranges = pinned_ranges(&seen); + assert_eq!( + ranges[ranges.len() - 2..], + [ + format!("bytes=0-{}", MAX_RANGE_BYTES - 1), + format!("bytes={}-{}", MAX_RANGE_BYTES, length - 1), + ], + "should hash all of B in pinned ranges" + ); + for read in &reads { + assert_eq!( + read.uri, + format!("https://{HOST}/media/ferry.png?edition=uk") + ); + assert_eq!(read.header("if-match"), Some(ETAG)); + let capability = read.header(CAPABILITY_HEADER).expect("should carry it"); + assert!( + verify_snapshot_capability( + PUBLICATION, + "/media/ferry.png?edition=uk", + capability, + unix_now(), + ) + .is_some(), + "the read capability should bind B's exact path and variant" + ); + assert!( + verify_snapshot_capability(PUBLICATION, "/media/ferry.png", capability, unix_now()) + .is_none(), + "B's capability should not authorize A" + ); + } + let recover = seen + .iter() + .position(|request| request.uri.ends_with("/media/hash/recover")) + .expect("should ask for the receipt after hashing B"); + let last_read = seen + .iter() + .rposition(|request| request.header(CAPABILITY_HEADER).is_some()) + .expect("should read B"); + assert!(recover > last_read); + assert!(kv.get(&result_key(&genuine.identity)).is_none()); +} + +#[test] +fn large_publication_refuses_reads_the_origin_facts_do_not_admit() { + let routes = routes(); + let source = vec![7_u8; 1024]; + let mut private = Origin::honest(source.clone()); + private.facts = Some(SnapshotOrigin { + private: true, + ..private.facts.clone().expect("should have facts") + }); + let mut unobserved = Origin::honest(source.clone()); + unobserved.facts = None; + let mut forged_facts = Origin::honest(source.clone()); + forged_facts.facts_secret = "an-origin-guessing-the-secret"; + let mut elsewhere = Origin::honest(source.clone()); + elsewhere.facts = Some(SnapshotOrigin { + origin_host: "cdn.other.example".to_owned(), + ..elsewhere.facts.clone().expect("should have facts") + }); + // The same host answers from another configured source, or reports none. + let mut remapped = Origin::honest(source.clone()); + remapped.facts = Some(SnapshotOrigin { + origin_identity_sha256: sha256_hex(REMAPPED_SOURCE.as_bytes()), + ..remapped.facts.clone().expect("should have facts") + }); + let mut unbound = Origin::honest(source.clone()); + unbound.facts = Some(SnapshotOrigin { + origin_identity_sha256: String::new(), + ..unbound.facts.clone().expect("should have facts") + }); + let mut encoded = Origin::honest(source.clone()); + encoded.facts = Some(SnapshotOrigin { + content_encoding: "gzip".to_owned(), + ..encoded.facts.clone().expect("should have facts") + }); + let mut provenance = Origin::honest(source.clone()); + provenance.facts = Some(SnapshotOrigin { + manifest_link: true, + ..provenance.facts.clone().expect("should have facts") + }); + let mut retagged = Origin::honest(source.clone()); + retagged.etag = "\"v2\""; + let mut retyped = Origin::honest(source.clone()); + retyped.content_type = "image/webp"; + + for (origin, status, code) in [ + (private, StatusCode::FORBIDDEN, "source_private"), + ( + unobserved, + StatusCode::UNPROCESSABLE_ENTITY, + "snapshot_unobserved", + ), + ( + forged_facts, + StatusCode::UNPROCESSABLE_ENTITY, + "snapshot_unobserved", + ), + ( + elsewhere, + StatusCode::UNPROCESSABLE_ENTITY, + "source_origin_mismatch", + ), + ( + remapped, + StatusCode::UNPROCESSABLE_ENTITY, + "source_identity_mismatch", + ), + ( + unbound, + StatusCode::UNPROCESSABLE_ENTITY, + "source_identity_mismatch", + ), + (encoded, StatusCode::UNPROCESSABLE_ENTITY, "content_encoded"), + ( + provenance, + StatusCode::UNPROCESSABLE_ENTITY, + "source_has_provenance_link", + ), + ( + retagged, + StatusCode::UNPROCESSABLE_ENTITY, + "source_version_changed", + ), + ( + retyped, + StatusCode::UNPROCESSABLE_ENTITY, + "source_type_mismatch", + ), + ] { + let http = ScriptedHttp::new(origin.responder()); + let kv = probe_kv(); + let (identity, genuine) = large_envelope(&routes, &source); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &genuine, + ); + assert_eq!(refused_with(response, status), code); + assert!( + http.api_calls().is_empty(), + "{code}: the API should never be asked" + ); + assert!( + kv.get(&result_key(&identity)).is_none(), + "{code}: nothing stored" + ); + } +} + +/// Publication binds the configured source, not only the host: a route an +/// operator remapped serves another resource under the same public URL, +/// host, entity tag, type and length. A result for source A is refused +/// against authenticated facts for source B, whether B answers every read +/// or only the reads after the first, before the API is asked or anything +/// is stored; the genuine source A still reaches receipt recovery. +#[test] +fn large_publication_refuses_a_version_another_configured_source_serves() { + let routes = routes(); + let length = usize::try_from(MAX_RANGE_BYTES).expect("should fit") + 10; + let source = large_png(length, 0); + let configured = sha256_hex(CONFIGURED_SOURCE.as_bytes()); + let remapped = sha256_hex(REMAPPED_SOURCE.as_bytes()); + let mut served_by_b = Origin::honest(source.clone()); + served_by_b.facts = Some(SnapshotOrigin { + origin_identity_sha256: remapped.clone(), + ..served_by_b.facts.clone().expect("should have facts") + }); + + // B answers every read: the first pinned read refuses A's result. + let http = ScriptedHttp::new(served_by_b.clone().responder()); + let kv = probe_kv(); + let (identity, genuine) = large_envelope(&routes, &source); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &genuine, + ); + assert_eq!( + refused_with(response, StatusCode::UNPROCESSABLE_ENTITY), + "source_identity_mismatch" + ); + assert_eq!( + pinned_ranges(&http.seen()).len(), + 1, + "should refuse at the first pinned read" + ); + assert!(http.api_calls().is_empty(), "the API should never be asked"); + assert!(kv.get(&result_key(&identity)).is_none(), "nothing stored"); + + // The remap lands after the first hashing range: every later read is + // checked again, so the rest of the version cannot come from B. + let switched = AtomicBool::new(false); + let first_hash_range = format!("bytes=0-{}", MAX_RANGE_BYTES - 1); + let from_a = Origin::honest(source.clone()).responder(); + let from_b = served_by_b.responder(); + let http = ScriptedHttp::new(move |seen| { + if switched.load(Ordering::SeqCst) { + return from_b(seen); + } + if seen.header("range") == Some(first_hash_range.as_str()) { + switched.store(true, Ordering::SeqCst); + } + from_a(seen) + }); + let kv = probe_kv(); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &genuine, + ); + assert_eq!( + refused_with(response, StatusCode::UNPROCESSABLE_ENTITY), + "source_identity_mismatch" + ); + assert_eq!( + pinned_ranges(&http.seen()).last().map(String::as_str), + Some(format!("bytes={}-{}", MAX_RANGE_BYTES, length - 1).as_str()), + "should refuse the hashing read B answered" + ); + assert!(http.api_calls().is_empty(), "the API should never be asked"); + assert!(kv.get(&result_key(&identity)).is_none(), "nothing stored"); + + // A worker that says it read B is refused where A answers. + let http = ScriptedHttp::new(Origin::honest(source.clone()).responder()); + let kv = probe_kv(); + let (claimed_b, envelope_b) = large_envelope_bound(&routes, &source, &remapped); + assert_ne!( + claimed_b, identity, + "B's result should have its own identity" + ); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &claimed_b, + &envelope_b, + ); + assert_eq!( + refused_with(response, StatusCode::UNPROCESSABLE_ENTITY), + "source_identity_mismatch" + ); + assert!(http.api_calls().is_empty(), "the API should never be asked"); + assert!(kv.get(&result_key(&claimed_b)).is_none(), "nothing stored"); + + // Genuine A passes every pinned read and the full hash, and only then + // asks for the receipt, which this API does not hold. + let http = ScriptedHttp::new(Origin::honest(source.clone()).responder()); + let kv = probe_kv(); + let (identity_a, genuine_a) = large_envelope_bound(&routes, &source, &configured); + assert_eq!(identity_a, identity); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity_a, + &genuine_a, + ); + assert_eq!( + refused_with(response, StatusCode::GONE), + "result_not_retained", + "A should pass every source check before the absent receipt refuses it" + ); + assert!( + http.seen() + .iter() + .any(|seen| seen.uri.ends_with("/media/hash/recover")), + "should ask for A's receipt" + ); +} + +/// The worker's configured-source digest must be one: an empty or +/// malformed value refuses the result before any source read. +#[test] +fn large_publication_refuses_a_malformed_configured_source_digest() { + let routes = routes(); + let source = vec![7_u8; 1024]; + let upper = sha256_hex(CONFIGURED_SOURCE.as_bytes()).to_ascii_uppercase(); + let short = "a".repeat(63); + let not_hex = "g".repeat(64); + for digest in ["", upper.as_str(), short.as_str(), not_hex.as_str()] { + let http = ScriptedHttp::new(Origin::honest(source.clone()).responder()); + let kv = probe_kv(); + let (identity, envelope) = large_envelope_bound(&routes, &source, digest); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &envelope, + ); + assert_eq!( + refused_with(response, StatusCode::UNPROCESSABLE_ENTITY), + "origin_identity_invalid", + "{digest:?}" + ); + assert!( + http.seen().is_empty(), + "{digest:?}: should neither read the source nor ask the API" + ); + assert!( + kv.get(&result_key(&identity)).is_none(), + "{digest:?}: nothing stored" + ); + } +} + +/// A large source whose XMP names a remote manifest keeps its provenance: +/// the publication inspects the pinned version itself, whatever the +/// worker's disclosure showed, and refuses before it reads the version in +/// full or asks the API anything. +#[test] +fn large_publication_keeps_the_provenance_the_pinned_version_carries() { + let routes = routes(); + let length = usize::try_from(MAX_RANGE_BYTES).expect("should fit") + 10; + let fill = length - png_with(&[]).len() - 12 - (12 + xmp_itxt(PROVENANCE_XMP).len()); + let xmp = xmp_itxt(PROVENANCE_XMP); + let filler = vec![7_u8; fill]; + let source = png_with(&[(b"iTXt", xmp.as_slice()), (b"prVt", filler.as_slice())]); + assert_eq!(source.len(), length); + let http = ScriptedHttp::new(Origin::honest(source.clone()).responder()); + let kv = probe_kv(); + let (identity, genuine) = large_envelope(&routes, &source); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &genuine, + ); + assert_eq!( + refused_with(response, StatusCode::UNPROCESSABLE_ENTITY), + "source_has_provenance_reference", + "the source's own provenance should be kept" + ); + assert!(http.api_calls().is_empty(), "the API should never be asked"); + assert!( + pinned_ranges(&http.seen()) + .iter() + .all(|range| within_block(range)), + "the version should never be read in full" + ); + assert!(kv.get(&result_key(&identity)).is_none(), "nothing stored"); +} + +// --- snapshots ------------------------------------------------------------ + +fn snapshot_request(path: &str, variant: &str, nonce: &str) -> Vec { + serde_json::to_vec(&SnapshotRequest { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: "test".to_owned(), + target: super::super::protocol::Target { + path: path.to_owned(), + variant: variant.to_owned(), + }, + nonce: nonce.to_owned(), + read: SnapshotMode::Whole, + }) + .expect("should encode the snapshot request") +} + +const PAGE: &str = "
Harbor council votes
"; + +fn page_origin(facts: Option, secret: &'static str) -> Arc { + ScriptedHttp::new(move |seen| { + let mut headers = vec![ + ("content-type", "text/html; charset=utf-8".to_owned()), + ("etag", "\"p1\"".to_owned()), + ("content-length", PAGE.len().to_string()), + ]; + if let (Some(facts), Some(capability)) = (&facts, seen.header(CAPABILITY_HEADER)) { + let nonce = capability.split('.').nth(2).expect("should carry a nonce"); + headers.push((FACTS_HEADER, facts_header(secret, nonce, facts))); + } + answer(200, PAGE.as_bytes().to_vec(), &headers) + }) +} + +fn public_page_facts() -> SnapshotOrigin { + SnapshotOrigin { + status: 200, + origin_host: HOST.to_owned(), + ..SnapshotOrigin::default() + } +} + +#[test] +fn a_snapshot_reads_one_configured_target_through_the_reader_pipeline() { + let routes = routes(); + let http = page_origin(Some(public_page_facts()), PUBLICATION); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + + let response = call( + &routes, + &services, + Method::POST, + SNAPSHOT_PATH, + Some(PUBLICATION), + snapshot_request("/news/a", "edition=uk", NONCE), + ); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get(header::CACHE_CONTROL) + .and_then(|value| value.to_str().ok()), + Some("private, no-store"), + "the snapshot's own transport should be private" + ); + let meta = response + .headers() + .get(SNAPSHOT_META_HEADER) + .and_then(|value| value.to_str().ok()) + .and_then(|value| STANDARD.decode(value).ok()) + .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) + .expect("should report the snapshot's metadata"); + assert_eq!(meta.page_url, "https://publisher.example.com/news/a"); + assert_eq!( + meta.origin, + public_page_facts(), + "should report the origin's own facts" + ); + assert_eq!(meta.etag.as_deref(), Some("\"p1\"")); + assert_eq!(body_of(response), PAGE.as_bytes()); + + let seen = http.seen(); + assert_eq!(seen.len(), 1, "should read once, through the loopback"); + let read = &seen[0]; + assert_eq!(read.uri, "https://publisher.example.com/news/a?edition=uk"); + assert_eq!(read.header("accept-encoding"), Some("identity")); + assert!(read.header("cookie").is_none() && read.header("authorization").is_none()); + assert_eq!( + verify_snapshot_capability( + PUBLICATION, + "/news/a?edition=uk", + read.header(CAPABILITY_HEADER) + .expect("should carry a capability"), + unix_now() + ) + .as_deref(), + Some(NONCE), + "the capability should name exactly this target and the worker's nonce" + ); +} + +#[test] +fn snapshots_admit_only_the_requested_range_or_an_exact_whole_response() { + let cases: &[(u16, u64, u64, &[&str], bool)] = &[ + (206, 8, 15, &["bytes 8-15/16"], true), + (206, 8, 15, &["bytes 0-7/16"], false), + (206, 8, 15, &["bytes 8-15/16", "bytes 8-15/16"], false), + (206, 8, 15, &["bytes 8-15/*"], false), + (200, 8, 15, &[], false), + (200, 0, 15, &[], true), + (206, 0, 15, &[], false), + ]; + for &(status, first, last, ranges, admitted) in cases { + let ranges: Vec = ranges.iter().map(|value| (*value).to_owned()).collect(); + let expected = b"abcdefghijklmnop"[first as usize..=last as usize].to_vec(); + let returned = expected.clone(); + let http = ScriptedHttp::new(move |seen| { + let capability = seen + .header(CAPABILITY_HEADER) + .expect("should authenticate the read"); + let nonce = capability + .split('.') + .nth(2) + .expect("should carry the nonce"); + let facts = SnapshotOrigin { + status, + origin_host: HOST.to_owned(), + ..SnapshotOrigin::default() + }; + let mut response = answer( + status, + returned.clone(), + &[ + ("etag", ETAG.to_owned()), + ("content-type", "video/mp4".to_owned()), + ("content-length", returned.len().to_string()), + (FACTS_HEADER, facts_header(PUBLICATION, nonce, &facts)), + ], + ); + for range in &ranges { + response.headers_mut().append( + header::CONTENT_RANGE, + HeaderValue::from_str(range).expect("should be a header value"), + ); + } + response + }); + let routes = routes(); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let request = SnapshotRequest { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: "test".to_owned(), + target: super::super::protocol::Target { + path: "/media/ferry.mp4".to_owned(), + variant: String::new(), + }, + nonce: NONCE.to_owned(), + read: SnapshotMode::Range { + first, + last, + if_match: ETAG.to_owned(), + }, + }; + let response = call( + &routes, + &services, + Method::POST, + SNAPSHOT_PATH, + Some(PUBLICATION), + serde_json::to_vec(&request).expect("should encode the range request"), + ); + if admitted { + assert_eq!(response.status(), StatusCode::OK); + let meta: SnapshotMeta = serde_json::from_slice( + &STANDARD + .decode(response.headers()[SNAPSHOT_META_HEADER].as_bytes()) + .expect("should encode metadata"), + ) + .expect("should report source metadata"); + assert_eq!(meta.length, Some(16)); + assert_eq!(body_of(response), expected); + } else { + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); + assert!(response.headers().get(SNAPSHOT_META_HEADER).is_none()); + } + } +} + +#[test] +fn a_snapshot_refuses_unconfigured_targets_before_reading_and_unadmitted_answers_after() { + let routes = routes(); + for (path, variant, nonce, status, code) in [ + ( + "/private/a", + "", + NONCE, + StatusCode::FORBIDDEN, + "target_not_configured", + ), + ( + "/news/a", + "edition=fr", + NONCE, + StatusCode::FORBIDDEN, + "target_not_configured", + ), + ( + "/news/a", + "utm_source=x", + NONCE, + StatusCode::FORBIDDEN, + "target_not_configured", + ), + ( + "/news/../private", + "", + NONCE, + StatusCode::FORBIDDEN, + "target_not_configured", + ), + ( + "/news/a", + "", + "short", + StatusCode::BAD_REQUEST, + "nonce_invalid", + ), + ] { + let http = ScriptedHttp::refusing(); + let kv = Arc::new(MemoryKv::default()); + let response = call( + &routes, + &services(&http, &kv), + Method::POST, + SNAPSHOT_PATH, + Some(PUBLICATION), + snapshot_request(path, variant, nonce), + ); + assert_eq!(refused_with(response, status), code, "{path}?{variant}"); + assert!(http.seen().is_empty(), "{path}: nothing should be read"); + } + + let private = SnapshotOrigin { + private: true, + ..public_page_facts() + }; + let linked = SnapshotOrigin { + manifest_link: true, + ..public_page_facts() + }; + for (http, status, code) in [ + ( + page_origin(None, PUBLICATION), + StatusCode::UNPROCESSABLE_ENTITY, + "snapshot_unobserved", + ), + ( + page_origin(Some(public_page_facts()), "an-origin-guessing-the-secret"), + StatusCode::UNPROCESSABLE_ENTITY, + "snapshot_unobserved", + ), + ( + page_origin(Some(private), PUBLICATION), + StatusCode::FORBIDDEN, + "source_private", + ), + ( + page_origin(Some(linked), PUBLICATION), + StatusCode::UNPROCESSABLE_ENTITY, + "source_has_provenance_link", + ), + ] { + let kv = Arc::new(MemoryKv::default()); + let response = call( + &routes, + &services(&http, &kv), + Method::POST, + SNAPSHOT_PATH, + Some(PUBLICATION), + snapshot_request("/news/a", "", NONCE), + ); + assert_eq!(refused_with(response, status), code); + } + + let http = ScriptedHttp::refusing(); + let kv = Arc::new(MemoryKv::default()); + let signal = call( + &routes, + &services(&http, &kv), + Method::POST, + SNAPSHOT_PATH, + Some(SIGNAL), + snapshot_request("/news/a", "", NONCE), + ); + assert_eq!( + refused_with(signal, StatusCode::UNAUTHORIZED), + "unauthorized", + "the reader-signal secret should not read sources" + ); + assert!(http.seen().is_empty()); +} + +// --- public source snapshots ---------------------------------------------- + +#[test] +fn a_public_source_is_served_only_as_the_exact_bytes_its_digest_names() { + let routes = routes(); + let sha = sha256_hex(SOURCE); + let http = ScriptedHttp::new(|_| { + answer( + 200, + SOURCE.to_vec(), + &[("content-type", "image/png".to_owned())], + ) + }); + let kv = Arc::new(MemoryKv::default()); + let response = call( + &routes, + &services(&http, &kv), + Method::GET, + &format!("{PUBLIC_SOURCE_PREFIX}{sha}"), + None, + Vec::new(), + ); + assert_eq!(response.status(), StatusCode::OK); + let headers = response.headers().clone(); + assert_eq!( + headers + .get("x-content-type-options") + .map(HeaderValue::as_bytes), + Some(&b"nosniff"[..]) + ); + assert!( + headers + .get(header::CONTENT_DISPOSITION) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.starts_with("attachment")), + "should never be rendered inline" + ); + assert_eq!( + headers + .get("content-security-policy") + .and_then(|value| value.to_str().ok()), + Some("sandbox; default-src 'none'; base-uri 'none'; form-action 'none'") + ); + assert_eq!(body_of(response), SOURCE); + let seen = http.seen(); + assert_eq!( + seen[0].uri, + format!("https://prep.publisher.example.com{WORKER_SOURCE_PREFIX}{sha}") + ); + assert_eq!(seen[0].header(TOKEN_HEADER), Some(PUBLICATION)); + + let lying = ScriptedHttp::new(|_| { + answer( + 200, + OTHER_SOURCE.to_vec(), + &[("content-type", "image/png".to_owned())], + ) + }); + let response = call( + &routes, + &services(&lying, &kv), + Method::GET, + &format!("{PUBLIC_SOURCE_PREFIX}{sha}"), + None, + Vec::new(), + ); + assert_eq!( + refused_with(response, StatusCode::NOT_FOUND), + "not_found", + "bytes that are not the digest's should never be served" + ); + + let http = ScriptedHttp::refusing(); + let response = call( + &routes, + &services(&http, &kv), + Method::GET, + &format!("{PUBLIC_SOURCE_PREFIX}not-a-digest"), + None, + Vec::new(), + ); + assert_eq!(refused_with(response, StatusCode::NOT_FOUND), "not_found"); + assert!(http.seen().is_empty()); +} + +// --- the request preparer ------------------------------------------------- + +fn settings(execution: &str) -> Settings { + page_settings(&format!( + "[provenance]\nmodule = \"encypher\"\n\n[provenance.encypher]\n\ + api_base_url = \"https://api.example.com\"\ndomain = \"{HOST}\"\n\ + worker_token = \"{TOKEN}\"\nexecution = \"{execution}\"\n\n\ + [provenance.encypher.preparation]\n\ + worker_url = \"https://prep.publisher.example.com\"\n\ + loopback_url = \"https://{HOST}\"\ndeployment_id = \"test\"\n\ + public_host = \"{HOST}\"\nsignal_secret = \"{SIGNAL}\"\n\ + publication_secret = \"{PUBLICATION}\"\nroutes = [\"/news/\"]\n\n\ + [[serve]]\nmedia_type = \"text/html\"\nmiddleware = [\"{MODULE}\"]\n" + )) +} + +fn prepared(settings: &Settings, path_and_query: &str, capabilities: &[&str]) -> Request { + let mut builder = Request::builder() + .method(Method::GET) + .uri(format!("https://{HOST}{path_and_query}")); + for capability in capabilities { + builder = builder.header(CAPABILITY_HEADER, *capability); + } + let mut request = builder.body(EdgeBody::empty()).expect("should build"); + prepare_request(settings, &mut request).expect("should prepare"); + request +} + +fn grant(request: &Request) -> Option { + IntegrationRequestState::of(request) + .get::(INTEGRATION_ID) + .map(|grant| grant.nonce.clone()) +} + +#[test] +fn only_a_verified_capability_for_the_exact_target_leaves_a_snapshot_grant() { + let asynchronous = settings("async_prepared"); + let expires = unix_now() + 60; + let genuine = snapshot_capability(PUBLICATION, "/news/a?edition=uk", NONCE, expires); + let other_path = snapshot_capability(PUBLICATION, "/news/b", NONCE, expires); + let forged = snapshot_capability( + "a-reader-guessing-the-secret", + "/news/a?edition=uk", + NONCE, + expires, + ); + + let request = prepared(&asynchronous, "/news/a?edition=uk", &[&genuine]); + assert_eq!(grant(&request).as_deref(), Some(NONCE)); + assert!( + request.headers().get(CAPABILITY_HEADER).is_none(), + "the capability should never travel on" + ); + assert!( + IntegrationRequestState::of(&request).is_empty(), + "a grant is an observation and keeps the reader's shared path" + ); + + for (name, path, capabilities) in [ + ( + "another target", + "/news/a?edition=uk", + vec![other_path.as_str()], + ), + ( + "another query", + "/news/a?edition=us", + vec![genuine.as_str()], + ), + ( + "a forged secret", + "/news/a?edition=uk", + vec![forged.as_str()], + ), + ( + "two values", + "/news/a?edition=uk", + vec![genuine.as_str(), genuine.as_str()], + ), + ( + "a malformed value", + "/news/a?edition=uk", + vec!["v1.not-a-capability"], + ), + ] { + let request = prepared(&asynchronous, path, &capabilities); + assert!(grant(&request).is_none(), "{name} should leave no grant"); + assert!( + request.headers().get(CAPABILITY_HEADER).is_none(), + "{name}: the header should be removed" + ); + } + + let synchronous = prepared(&settings("synchronous"), "/news/a?edition=uk", &[&genuine]); + assert!( + grant(&synchronous).is_none(), + "the synchronous mode grants nothing" + ); + assert!( + synchronous.headers().get(CAPABILITY_HEADER).is_none(), + "the header is removed whether or not the asynchronous mode runs" + ); +} + +// --- real probe receipts -------------------------------------------------- + +fn fixture(relative: &str) -> Vec { + let bytes: &[u8] = match relative { + "probe.wasm" => include_bytes!("../../fixtures/prepared/probe.wasm"), + "jpeg/envelope.json" => include_bytes!("../../fixtures/prepared/jpeg/envelope.json"), + "jpeg/recover.json" => include_bytes!("../../fixtures/prepared/jpeg/recover.json"), + "png/envelope.json" => include_bytes!("../../fixtures/prepared/png/envelope.json"), + "png/recover.json" => include_bytes!("../../fixtures/prepared/png/recover.json"), + "webp/envelope.json" => include_bytes!("../../fixtures/prepared/webp/envelope.json"), + "webp/recover.json" => include_bytes!("../../fixtures/prepared/webp/recover.json"), + "svg/envelope.json" => include_bytes!("../../fixtures/prepared/svg/envelope.json"), + "svg/recover.json" => include_bytes!("../../fixtures/prepared/svg/recover.json"), + "external-png/envelope.json" => { + include_bytes!("../../fixtures/prepared/external-png/envelope.json") + } + "external-png/recover.json" => { + include_bytes!("../../fixtures/prepared/external-png/recover.json") + } + _ => panic!("unknown prepared fixture: {relative}"), + }; + bytes.to_vec() +} + +/// The routes of a deployment claiming the host `envelope` names, with its +/// identity and deployment rewritten to that deployment's own. +fn deployment_for(envelope: &mut ResultEnvelope) -> PreparationRoutes { + let planned = match &envelope.item { + EnvelopeItem::Media { planned, .. } | EnvelopeItem::Embedded { planned, .. } => planned, + _ => panic!("should be a bounded media case"), + }; + let page = url::Url::parse(&planned.request.page_url).expect("should be a URL"); + let host = page.host_str().expect("should name a host").to_owned(); + let domain = host.strip_prefix("www.").unwrap_or(&host).to_owned(); + let routes = routes_for(config_value(&domain, &host, &["/"])); + envelope.deployment_id = "test".to_owned(); + envelope.identity = media_identity(&routes, &planned.request, &envelope.variant); + routes +} + +/// A transport answering as the candidate API answered the worker. +fn candidate_api(recover: Vec, probe: Vec) -> Arc { + ScriptedHttp::new(move |seen| { + if seen.uri.ends_with("/api/v1/public/cdn/media/hash/recover") { + answer( + 200, + recover.clone(), + &[("content-type", "application/json".to_owned())], + ) + } else if seen.uri.contains("/api/v1/sign/media/probe/") { + answer(200, probe.clone(), &[]) + } else { + answer(500, Vec::new(), &[]) + } + }) +} + +fn case_envelope(case: &str) -> ResultEnvelope { + serde_json::from_slice(&fixture(&format!("{case}/envelope.json"))) + .expect("should be a result envelope") +} + +/// Publishes `envelope` against `case`'s recorded receipt, returning the +/// answer and whether a record was stored. +fn publish_against(case: &str, mut envelope: ResultEnvelope) -> (Response, bool) { + let routes = deployment_for(&mut envelope); + let http = candidate_api( + fixture(&format!("{case}/recover.json")), + fixture("probe.wasm"), + ); + let kv = Arc::new(MemoryKv::default()); + let identity = envelope.identity.clone(); + let response = publish( + &routes, + &services(&http, &kv), + PUBLICATION, + &identity, + &envelope, + ); + assert!( + http.seen() + .iter() + .all(|seen| !seen.uri.ends_with("/hash/finalize") + && !seen.uri.ends_with("/hash/prepare") + && !seen.uri.ends_with("/cdn/sign")), + "{case}: publication should never prepare, finalize or sign" + ); + let stored = kv.get(&result_key(&identity)).is_some(); + (response, stored) +} + +fn flip_last(b64: &str) -> String { + let mut bytes = STANDARD.decode(b64).expect("should be base64"); + *bytes.last_mut().expect("should have bytes") ^= 1; + STANDARD.encode(bytes) +} + +/// Rewrites an embedded envelope's source to `source`, keeping the request +/// self-consistent with it, as a forging worker would. +fn with_source(mut envelope: ResultEnvelope, source: &[u8]) -> ResultEnvelope { + if let EnvelopeItem::Embedded { + planned, + source_b64, + .. + } = &mut envelope.item + { + planned.request.source.sha256 = sha256_hex(source); + planned.request.source.byte_length = u64::try_from(source.len()).expect("should fit"); + *source_b64 = STANDARD.encode(source); + } + envelope +} + +fn embedded_parts(envelope: &ResultEnvelope) -> (Vec, String) { + match &envelope.item { + EnvelopeItem::Embedded { + source_b64, + output_b64, + .. + } => ( + STANDARD.decode(source_b64).expect("should be base64"), + output_b64.clone(), + ), + _ => panic!("should be an embedded case"), + } +} + +/// The real probe, run on candidate-API receipts for JPEG, PNG, WebP and +/// inline SVG: the genuine output publishes, and every forgery of output or +/// source, however self-consistent, is refused before anything is stored. +#[test] +fn real_probe_receipts_publish_only_the_exact_output() { + let cases = ["jpeg", "png", "webp", "svg"]; + for case in cases { + let genuine = case_envelope(case); + let (response, stored) = publish_against(case, genuine.clone()); + assert_eq!( + response.status(), + StatusCode::OK, + "{case}: should publish: {}", + String::from_utf8_lossy(&body_of(response)) + ); + assert!(stored, "{case}: should store the verified record"); + + // One byte of the output changed. + let mut tampered = genuine.clone(); + if let EnvelopeItem::Embedded { output_b64, .. } = &mut tampered.item { + *output_b64 = flip_last(output_b64); + } + let (response, stored) = publish_against(case, tampered); + assert_eq!( + response.status(), + StatusCode::UNPROCESSABLE_ENTITY, + "{case}: a one-byte output change should be refused" + ); + assert!(!stored, "{case}: nothing stored for a tampered output"); + + // Another case's genuine output substituted. + let donor = cases + .iter() + .find(|other| **other != case) + .expect("should have another case"); + let (_, donor_output) = embedded_parts(&case_envelope(donor)); + let mut substituted = genuine.clone(); + if let EnvelopeItem::Embedded { output_b64, .. } = &mut substituted.item { + *output_b64 = donor_output; + } + let (response, stored) = publish_against(case, substituted); + assert_eq!( + response.status(), + StatusCode::UNPROCESSABLE_ENTITY, + "{case}: a substituted output should be refused" + ); + assert!(!stored); + + // Changed pixels or markup with a self-consistent request: the + // receipt is genuine, the source is not the one it signed. + let (mut source, _) = embedded_parts(&genuine); + let middle = source.len() / 2; + source[middle] ^= 1; + let (response, stored) = publish_against(case, with_source(genuine.clone(), &source)); + assert_eq!( + response.status(), + StatusCode::UNPROCESSABLE_ENTITY, + "{case}: a changed source should be refused by the probe's checks" + ); + assert!(!stored); + } +} + +#[test] +fn real_probe_receipt_refuses_tampered_external_source() { + let genuine = case_envelope("external-png"); + let (response, stored) = publish_against("external-png", genuine.clone()); + assert_eq!( + response.status(), + StatusCode::OK, + "external: should publish: {}", + String::from_utf8_lossy(&body_of(response)) + ); + assert!(stored); + let mut tampered = genuine; + if let EnvelopeItem::Media { source_b64, .. } = &mut tampered.item { + *source_b64 = flip_last(source_b64); + } + let (response, stored) = publish_against("external-png", tampered); + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); + assert!(!stored); +} + +/// A bounded source whose XMP names a remote manifest keeps its +/// provenance: an external manifest delivered over it would never be read. +/// The receipt is the fixture's own, for the source without that packet. No +/// genuine receipt for a provenance-bearing source exists, so the refusal +/// must come from the source itself, before the receipt is checked. +#[test] +fn real_probe_external_publication_keeps_the_source_provenance() { + let xmp = xmp_itxt(PROVENANCE_XMP); + let source = png_with(&[(b"iTXt", xmp.as_slice())]); + let mut envelope = case_envelope("external-png"); + let EnvelopeItem::Media { + planned, + source_b64, + } = &mut envelope.item + else { + panic!("should be an external case"); + }; + planned.request.source.sha256 = sha256_hex(&source); + planned.request.source.byte_length = u64::try_from(source.len()).expect("should fit"); + planned.digest = planned.request.source.sha256.clone(); + *source_b64 = STANDARD.encode(&source); + let (response, stored) = publish_against("external-png", envelope); + assert_eq!( + refused_with(response, StatusCode::UNPROCESSABLE_ENTITY), + "source_has_provenance_reference", + "the source's own provenance should be kept" + ); + assert!(!stored, "nothing stored"); +} + +// --- validators ----------------------------------------------------------- + +#[test] +fn only_a_single_strong_validator_pins_a_source() { + let mut headers = http::HeaderMap::new(); + headers.insert(header::ETAG, HeaderValue::from_static("\"v1\"")); + assert_eq!(single_strong_etag(&headers).as_deref(), Some("\"v1\"")); + headers.append(header::ETAG, HeaderValue::from_static("\"v2\"")); + assert!( + single_strong_etag(&headers).is_none(), + "two tags name no single version" + ); + let mut weak = http::HeaderMap::new(); + weak.insert(header::ETAG, HeaderValue::from_static("W/\"v1\"")); + assert!( + single_strong_etag(&weak).is_none(), + "a weak tag pins nothing" + ); +} diff --git a/crates/provenance/encypher/src/preparation/items.rs b/crates/provenance/encypher/src/preparation/items.rs new file mode 100644 index 0000000000..35d559272f --- /dev/null +++ b/crates/provenance/encypher/src/preparation/items.rs @@ -0,0 +1,391 @@ +//! The items of one delivered document, with their preparation identities. +//! +//! The reader computes these from the bytes it is about to deliver and the +//! worker from the snapshot the delivery instance gave it, through the same +//! functions, so a stable document yields the same identities on both. +//! Text comes from the one article extraction every path uses; embedded +//! media from the one scan of `data:` URIs and inline SVG that decides +//! which ranges may be edited. + +use std::borrow::Cow; + +use super::pipeline::{TextCacheScope, TextSignRequest}; +use super::protocol::{Carrier, MediaIdentity, TextIdentity}; +use crate::data_uri::DataUri; +use crate::embedded; +use crate::media_api::sha256_hex; +use crate::processor::{Article, Reason, extract_article}; +use crate::svg_equivalence; +use crate::text; + +/// What a document is, which decides what in it is an item. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DocumentKind { + /// An HTML page: its article's text and its embedded media. + Page, + /// A stylesheet: the media its `url()`s embed. + Stylesheet, +} + +/// What every identity of one document binds besides the item itself. +#[derive(Debug, Clone, Copy)] +pub struct ItemContext<'a> { + /// See [`super::protocol::credential_context`]. + pub credential_context: &'a str, + /// The policy the items are prepared under. + pub policy_digest: &'a str, + /// The query-free address the document is delivered on. + pub page_url: &'a str, + /// The admitted variant. + pub variant: &'a str, + /// The configured article selector override, empty for none. + pub article_selector: &'a str, + /// The least visible text an article is signed with. + pub min_text_length: usize, + /// The largest embedded media item. + pub max_media_bytes: usize, +} + +/// An article's text item. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TextItem { + /// Its identity. + pub identity: String, + /// The exact sign request it is prepared with. + pub request: TextSignRequest, + /// The scalars of its text. + pub scalars: usize, +} + +/// Where an embedded item sits in its document. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Site { + /// A media `data:` URI, its exact byte range. + DataUri { + /// The first byte. + start: usize, + /// One past the last byte. + end: usize, + }, + /// An outermost inline ``, its exact byte range. + InlineSvg { + /// The first byte. + start: usize, + /// One past the last byte. + end: usize, + }, +} + +impl Site { + fn range(self) -> (usize, usize) { + match self { + Self::DataUri { start, end } | Self::InlineSvg { start, end } => (start, end), + } + } +} + +/// One embedded media item. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmbeddedItem { + /// Its identity. + pub identity: String, + /// Where it sits. + pub site: Site, + /// Its canonical media type. + pub mime: String, + /// Its exact source bytes: the decoded payload, or the standalone SVG + /// document proven equivalent to the browser's tree. + pub source: Vec, + /// SHA-256 of `source`. + pub sha256: String, +} + +/// The items of one document. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct DocumentItems { + /// The article, where the page has one. + pub text: Option, + /// Why the page's article is not an item, where it is not. + pub text_refusal: Option<&'static str>, + /// Embedded media, in document order, never overlapping. + pub embedded: Vec, + /// Embedded media left as written, each with why. + pub refused: Vec<&'static str>, +} + +/// The identity of an article, without constructing an owned signing request. +#[must_use] +pub(crate) fn text_identity( + context: &ItemContext<'_>, + article: &Article, + document_title: Option<&str>, +) -> String { + TextIdentity { + credential_context: context.credential_context, + policy_digest: context.policy_digest, + page_url: context.page_url, + variant: context.variant, + text: &article.text, + document_title, + boundary_selector: &article.boundary.selector, + } + .digest() +} + +/// The text item of `article` in `html`. +#[must_use] +pub(crate) fn text_item(context: &ItemContext<'_>, html: &str, article: &Article) -> TextItem { + let title = text::title_from_range(html, article.title_range); + let identity = text_identity(context, article, title.as_deref()); + TextItem { + identity, + request: TextSignRequest { + text: article.text.clone(), + page_url: context.page_url.to_owned(), + document_title: title, + boundary_selector: article.boundary.selector.clone(), + expected_policy_digest: context.policy_digest.to_owned(), + cache_scope: TextCacheScope::ExactRequest, + }, + scalars: article.scalars, + } +} + +/// The identity of an embedded item with `mime` and `sha256` in a document +/// of `context`. +#[must_use] +pub fn embedded_identity(context: &ItemContext<'_>, mime: &str, sha256: &str) -> String { + MediaIdentity { + credential_context: context.credential_context, + policy_digest: context.policy_digest, + page_url: context.page_url, + variant: context.variant, + carrier: Carrier::Embedded, + mime, + source_sha256: sha256, + } + .digest() +} + +/// The embedded media items of `document`. An inline SVG is one item, and +/// a `data:` URI inside an inline SVG item is carried by it rather than +/// prepared on its own, so no two items overlap and an SVG's identity is +/// that of the markup the browser parsed. +#[must_use] +pub fn embedded_items( + context: &ItemContext<'_>, + document: &str, + kind: DocumentKind, + policies: Option<&[String]>, +) -> (Vec, Vec<&'static str>) { + let scan = match kind { + DocumentKind::Page => embedded::scan_html(document, policies), + DocumentKind::Stylesheet => embedded::scan_stylesheet(document), + }; + let mut refused: Vec<&'static str> = scan.refused.iter().map(|reason| reason.code()).collect(); + let mut items = Vec::new(); + let mut svg_ranges = Vec::new(); + if kind == DocumentKind::Page { + for (start, end) in scan.svgs.iter().copied() { + let Some(fragment) = document.get(start..end) else { + continue; + }; + match svg_equivalence::signable_svg(fragment, context.max_media_bytes) { + Ok(svg) => { + let source = match svg { + Cow::Borrowed(text) => text.as_bytes().to_vec(), + Cow::Owned(text) => text.into_bytes(), + }; + let sha256 = sha256_hex(&source); + svg_ranges.push((start, end)); + items.push(EmbeddedItem { + identity: embedded_identity(context, "image/svg+xml", &sha256), + site: Site::InlineSvg { start, end }, + mime: "image/svg+xml".to_owned(), + source, + sha256, + }); + } + Err(reason) => refused.push(reason.code()), + } + } + } + for found in &scan.found { + if svg_ranges + .iter() + .any(|(start, end)| found.start < *end && *start < found.end) + { + refused.push("inside_inline_svg"); + continue; + } + let Some(uri) = document + .get(found.start..found.end) + .and_then(DataUri::parse) + else { + continue; + }; + let Ok(source) = uri.decode() else { + refused.push(Reason::MalformedDataUri.code()); + continue; + }; + if source.len() > context.max_media_bytes { + refused.push(Reason::MediaTooLarge.code()); + continue; + } + let mime = uri.media_type(); + let sha256 = sha256_hex(&source); + items.push(EmbeddedItem { + identity: embedded_identity(context, &mime, &sha256), + site: Site::DataUri { + start: found.start, + end: found.end, + }, + mime, + source, + sha256, + }); + } + items.sort_by_key(|item| item.site.range()); + (items, refused) +} + +/// Every item of `document`: the article's text first, then its embedded +/// media. +#[must_use] +pub fn document_items( + context: &ItemContext<'_>, + document: &str, + kind: DocumentKind, + policies: Option<&[String]>, +) -> DocumentItems { + let (text, text_refusal) = match kind { + DocumentKind::Page => { + match extract_article(document, context.article_selector, context.min_text_length) { + Ok(article) => (Some(text_item(context, document, &article)), None), + Err(reason) => (None, Some(reason.code())), + } + } + DocumentKind::Stylesheet => (None, None), + }; + let (embedded, refused) = embedded_items(context, document, kind, policies); + DocumentItems { + text, + text_refusal, + embedded, + refused, + } +} + +/// The edit that puts an embedded item's output in place of its source in +/// `document`, or `None` where the output does not fit the site: an SVG +/// that is not UTF-8. +#[must_use] +pub fn embedded_edit( + document: &str, + item: &EmbeddedItem, + output: &[u8], +) -> Option<(usize, usize, String)> { + match item.site { + Site::DataUri { start, end } => { + let uri = DataUri::parse(document.get(start..end)?)?; + Some((start, end, uri.encode(output))) + } + Site::InlineSvg { start, end } => { + Some((start, end, String::from_utf8(output.to_vec()).ok()?)) + } + } +} + +#[cfg(test)] +mod tests { + use base64::Engine as _; + use base64::engine::general_purpose::STANDARD; + + use super::*; + + const POLICY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn context() -> ItemContext<'static> { + ItemContext { + credential_context: "0123456789abcdef", + policy_digest: POLICY, + page_url: "https://www.publisher.example.com/news/a", + variant: "", + article_selector: "", + min_text_length: 50, + max_media_bytes: 1024 * 1024, + } + } + + fn page(pixel: &[u8]) -> String { + format!( + "Harbor
\ +

Harbor council votes on the ferry schedule

The harbor council met on \ + Tuesday evening and approved a revised ferry schedule for the season.

\ +
", + STANDARD.encode(pixel) + ) + } + + #[test] + fn identical_documents_yield_identical_items_and_one_changed_item_changes_only_its_own() { + let first = document_items(&context(), &page(b"pixel-one"), DocumentKind::Page, None); + let again = document_items(&context(), &page(b"pixel-one"), DocumentKind::Page, None); + assert_eq!( + first, again, + "a stable document should yield the same items" + ); + let text = first.text.as_ref().expect("should find the article"); + assert_eq!(first.embedded.len(), 1, "should find the embedded image"); + + let changed = document_items(&context(), &page(b"pixel-two"), DocumentKind::Page, None); + assert_eq!( + changed.text.as_ref().map(|item| &item.identity), + Some(&text.identity), + "a changed image should leave the article's identity unchanged" + ); + assert_ne!( + changed.embedded[0].identity, first.embedded[0].identity, + "the changed image should take a new identity" + ); + } + + #[test] + fn policy_change_changes_every_identity() { + let document = page(b"pixel-one"); + let first = document_items(&context(), &document, DocumentKind::Page, None); + let other_policy = "b".repeat(64); + let changed = document_items( + &ItemContext { + policy_digest: &other_policy, + ..context() + }, + &document, + DocumentKind::Page, + None, + ); + assert_ne!( + first.text.map(|item| item.identity), + changed.text.map(|item| item.identity), + "a new policy should never reuse a result made under the old one" + ); + assert_ne!(first.embedded[0].identity, changed.embedded[0].identity); + } + + #[test] + fn embedded_edit_replaces_only_the_items_own_range() { + let document = page(b"pixel-one"); + let items = document_items(&context(), &document, DocumentKind::Page, None); + let item = &items.embedded[0]; + let (start, end, text) = + embedded_edit(&document, item, b"signed-pixel").expect("should build the edit"); + assert!( + text.starts_with("data:image/png;base64,"), + "should keep the URI's own type" + ); + let output = + embedded::splice(&document, &[(start, end, text)], usize::MAX).expect("should splice"); + assert!(output.contains(&STANDARD.encode(b"signed-pixel"))); + assert!(!output.contains(&STANDARD.encode(b"pixel-one"))); + } +} diff --git a/crates/provenance/encypher/src/preparation/mod.rs b/crates/provenance/encypher/src/preparation/mod.rs new file mode 100644 index 0000000000..82b5c7d014 --- /dev/null +++ b/crates/provenance/encypher/src/preparation/mod.rs @@ -0,0 +1,197 @@ +//! Asynchronous prepared signing: content versions are signed by the +//! publisher's native preparation worker, outside reader delivery, and +//! readers only look up what the delivery instance verified and published. +//! +//! In `execution = "async_prepared"` a reader never signs, prepares, +//! finalizes or uploads. It computes each item's identity from the exact +//! projection or bytes it delivers, looks up a published result under that +//! immutable identity, and on a miss delivers the current version unsigned +//! and hands the worker a bounded hint. A prior version's result is never +//! applied to new bytes, and the mode never falls back to signing in +//! delivery. +//! +//! [`protocol`] holds the identities and messages both sides agree on, +//! [`items`] the one extraction of a document's items both sides run, and +//! [`pipeline`] the prepared-hash steps the worker runs and the delivery +//! instance re-verifies through the authenticated API before it publishes +//! anything. + +pub mod items; +pub mod pipeline; +pub mod protocol; + +pub(crate) mod edge; + +use serde::{Deserialize, Serialize}; +use validator::{Validate, ValidationError}; + +use trusted_server_core::redacted::Redacted; + +pub use crate::api::{EmbeddingPlan, PlanOperation}; +pub use protocol::{VariantParam, VariantSchema}; + +/// How the module signs. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Execution { + /// Each reader's delivery signs what it has no cached result for. The + /// existing mode, kept as a deliberate operator choice. + #[default] + Synchronous, + /// The publisher's preparation worker signs; delivery only looks up + /// published results and never signs. + AsyncPrepared, +} + +/// `[provenance.encypher.preparation]`, the asynchronous mode's settings. +#[derive(Debug, Clone, Deserialize, Validate)] +#[serde(deny_unknown_fields)] +pub struct PreparationConfig { + /// The preparation worker's origin, `https`, or `http` on a loopback + /// host for a local deployment. Reader hints and public source reads go + /// here, never with a reader's credentials. + #[validate(custom(function = validate_service_url))] + pub worker_url: String, + /// The origin the delivery instance reads its own snapshots from: its + /// public `https` address, or `http` on a loopback host. While + /// `[asset-cache]` selects a module, exactly `https://`, with + /// at most a trailing `/` or the default port `:443`, so a snapshot reads + /// the copy the asset cache holds for readers. + #[validate(custom(function = validate_service_url))] + pub loopback_url: String, + /// The deployment's identifier, which every message carries. + #[validate(custom(function = validate_deployment_id))] + pub deployment_id: String, + /// The host readers are served on and every prepared address names: + /// the claimed domain or its `www.` name. Readers on another host find + /// no published result. + pub public_host: String, + /// The secret-store entry of the reader-signal scope, resolved to the + /// secret as settings load. It authorizes hints and nothing else. + pub signal_secret: Redacted, + /// The secret-store entry of the snapshot and publication scope, + /// resolved as settings load. It authorizes snapshots, result and marker + /// publication and the worker's public source reads. + pub publication_secret: Redacted, + /// Whether readers hint the worker about unprepared items. Needs a + /// platform with a real timer or an after-response lifetime; refused at + /// registration on one with neither. Publication events, warming and + /// reconciliation prepare content either way. + #[serde(default)] + pub reader_trigger: bool, + /// The longest a reader's hint may take on a platform with a timer, in + /// milliseconds. + #[serde(default = "default_admission_ms")] + #[validate(range(min = 10, max = 200))] + pub admission_ms: u64, + /// Path prefixes snapshots may read and hints may name. Nothing else is + /// ever read for preparation. + #[validate(length(min = 1, max = 64), custom(function = validate_routes))] + pub routes: Vec, + /// The bounded variant schema: query parameters and the values that + /// name a variant. A reader query naming anything else is not hinted. + #[serde(default)] + #[validate(custom(function = validate_variants))] + pub variants: VariantSchema, +} + +fn default_admission_ms() -> u64 { + 50 +} + +/// Whether `value` is an `https` origin, or `http` on a loopback host, with +/// no path, query, fragment or user information. +fn service_url_ok(value: &str) -> bool { + let Ok(parsed) = url::Url::parse(value) else { + return false; + }; + let loopback = matches!(parsed.host_str(), Some("127.0.0.1" | "localhost" | "[::1]")); + let scheme_ok = parsed.scheme() == "https" || (parsed.scheme() == "http" && loopback); + scheme_ok + && parsed.host_str().is_some() + && parsed.username().is_empty() + && parsed.password().is_none() + && parsed.query().is_none() + && parsed.fragment().is_none() + && matches!(parsed.path(), "" | "/") +} + +fn validate_service_url(value: &str) -> Result<(), ValidationError> { + if service_url_ok(value) { + Ok(()) + } else { + Err(ValidationError::new("preparation_url")) + } +} + +fn validate_deployment_id(value: &str) -> Result<(), ValidationError> { + let ok = !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-'); + if ok { + Ok(()) + } else { + Err(ValidationError::new("deployment_id")) + } +} + +fn validate_routes(routes: &[String]) -> Result<(), ValidationError> { + let ok = routes.iter().all(|route| { + route.starts_with('/') + && route.len() <= protocol::MAX_TARGET_PATH_BYTES + && route + .bytes() + .all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'?' | b'#')) + }); + if ok { + Ok(()) + } else { + Err(ValidationError::new("preparation_routes")) + } +} + +fn validate_variants(variants: &VariantSchema) -> Result<(), ValidationError> { + if variants.is_bounded() { + Ok(()) + } else { + Err(ValidationError::new("preparation_variants")) + } +} + +impl PreparationConfig { + /// Whether `path` lies under a configured route. + #[must_use] + pub fn admits_path(&self, path: &str) -> bool { + self.routes + .iter() + .any(|route| path.starts_with(route.as_str())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn service_urls_are_https_or_loopback_http_origins_only() { + for good in [ + "https://prep.example.com", + "https://prep.example.com/", + "http://127.0.0.1:8181", + "http://localhost:8181", + ] { + assert!(service_url_ok(good), "should admit {good}"); + } + for bad in [ + "http://prep.example.com", + "https://user:pw@prep.example.com", + "https://prep.example.com/path", + "https://prep.example.com/?q=1", + "ftp://prep.example.com", + ] { + assert!(!service_url_ok(bad), "should refuse {bad}"); + } + } +} diff --git a/crates/provenance/encypher/src/preparation/pipeline.rs b/crates/provenance/encypher/src/preparation/pipeline.rs new file mode 100644 index 0000000000..39e699b35e --- /dev/null +++ b/crates/provenance/encypher/src/preparation/pipeline.rs @@ -0,0 +1,2410 @@ +//! The prepared-signing steps the worker runs and the delivery instance +//! re-verifies, over any HTTP transport and any source reader. +//! +//! Nothing here uploads source bytes. A media item is planned from its +//! structure disclosure, hashed locally by the hash program the signed +//! probe verified, and finalized by digest. An external manifest binds the +//! whole unchanged source; an embedded one is carried in output bytes +//! assembled here from the verified edits and the locally built carrier, +//! then walked by the probe's `verify_output` so its binding reproduces the +//! digest the service signed. Every refusal is a [`Failure`] with a class +//! the worker damps by. +//! +//! The delivery instance never trusts what the worker says it signed. It +//! recovers the API's exact completed receipt with +//! [`recover_media`] or [`recover_text`], and checks it against the source +//! bytes and output with [`verify_external_receipt`] and +//! [`verify_embedded_receipt`]. + +use std::convert::Infallible; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use bytes::Bytes; +use http::Method; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use sha2::{Digest as _, Sha256}; + +use super::protocol::{Carrier, PublishedResult}; +use crate::api::{self, EmbeddingPlan}; +use crate::embedded; +use crate::media_api::{hex_lower, sha256_hex}; +use crate::prepared_probe::{ + ProbeCallError, ProbeDescriptor, ProbeLimits, ProbeModule, ProbeSource, ReleaseAuthority, +}; + +/// The prepared protocol every prepared request carries. +pub const PREPARED_PROTOCOL: &str = "encypher-prepared-signing/2"; +/// The worker contract version this pipeline speaks. +pub const CONTRACT_VERSION: u8 = 2; + +const POLICY_PATH: &str = "/api/v1/public/cdn/trusted-server/policy"; +const SIGN_PATH: &str = "/api/v1/public/cdn/sign"; +const SIGN_RECOVER_PATH: &str = "/api/v1/public/cdn/sign/recover"; +const CAPABILITIES_PATH: &str = "/api/v1/public/cdn/media/hash/capabilities"; +const PREPARE_PATH: &str = "/api/v1/public/cdn/media/hash/prepare"; +const FINALIZE_PATH: &str = "/api/v1/public/cdn/media/hash/finalize"; +const MEDIA_RECOVER_PATH: &str = "/api/v1/public/cdn/media/hash/recover"; +const PROBE_PATH: &str = "/api/v1/"; + +const MAX_POLICY_BYTES: usize = 1024; +const MAX_CAPABILITIES_BYTES: usize = 64 * 1024; +const MAX_PREPARE_BYTES: usize = 1024 * 1024; +const MAX_PROBE_BYTES: usize = 4 * 1024 * 1024; +const MAX_FINALIZED_MANIFEST_BYTES: usize = 2 * 1024 * 1024; +const FINALIZE_OVERHEAD_BYTES: usize = 64 * 1024; +/// The contract ceiling on one `insert_bytes` payload. +const MAX_INSERT_BYTES: usize = 65_536; +/// The contract ceilings on one structure disclosure: its chunks and their +/// decoded bytes. +const MAX_STRUCTURE_CHUNKS: usize = 65_536 + 8; +const MAX_STRUCTURE_BYTES: usize = 32 * 1024 * 1024; +/// The most an embedded output may exceed its source by: the largest +/// manifest store plus as much again for its carrier framing and the +/// plan's inserted bytes. A plan over it is refused before anything is +/// allocated or finalized, and a worker reserves at most this much beyond +/// the source for an output. +pub const MAX_OUTPUT_GROWTH_BYTES: u64 = 2 * MAX_FINALIZED_MANIFEST_BYTES as u64; + +/// The largest output an embedded plan over a source of `source_length` +/// bytes may name. +#[must_use] +pub fn output_ceiling(source_length: u64) -> u64 { + source_length.saturating_add(MAX_OUTPUT_GROWTH_BYTES) +} + +/// How a failure is damped by the worker. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FailureClass { + /// May succeed on a later attempt: transport, rate limit, server error. + Transient, + /// The policy changed under the operation; a new identity follows. + PolicyChanged, + /// A binding, plan, carrier, store or output check failed. + Integrity, + /// The source is one reader's, or not admitted as public. + Privacy, + /// The format, carrier or contract is not supported by the engine. + Unsupported, + /// The source carries provenance of its own, which is kept. + ExistingProvenance, + /// The API holds no completed result for the operation. + Missing, +} + +/// Why a step did not complete. +#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] +#[display("{code} ({class:?})")] +pub struct Failure { + /// A short stable code. + pub code: String, + /// How the worker damps it. + pub class: FailureClass, +} + +impl core::error::Error for Failure {} + +impl Failure { + /// A failure of `class` with `code`. + #[must_use] + pub fn new(class: FailureClass, code: &str) -> Self { + Self { + code: code.to_owned(), + class, + } + } + + fn integrity(code: &str) -> Self { + Self::new(FailureClass::Integrity, code) + } + + fn transient(code: &str) -> Self { + Self::new(FailureClass::Transient, code) + } + + fn unsupported(code: &str) -> Self { + Self::new(FailureClass::Unsupported, code) + } +} + +/// One request to the Encypher API. +#[derive(Debug, Clone)] +pub struct ApiRequest { + /// The method. + pub method: Method, + /// The path and query below the API origin. + pub path_and_query: String, + /// A JSON body, if any. + pub body: Option>, + /// The `Idempotency-Key`, if any. + pub idempotency_key: Option, + /// Whether the worker credential is sent. A probe artifact is fetched + /// without it. + pub credentialed: bool, + /// The most response body bytes read. + pub max_response_bytes: usize, +} + +/// The API's answer: its status and bounded body. +#[derive(Debug, Clone)] +pub struct ApiAnswer { + /// The status. + pub status: u16, + /// The body, at most the request's bound plus one byte. + pub body: Bytes, +} + +/// An HTTP transport to the Encypher API, the edge's platform client on the +/// delivery instance and a native client on the worker. +#[allow(async_fn_in_trait)] +pub trait ApiTransport { + /// Sends `request` once. + /// + /// # Errors + /// + /// A transient [`Failure`] where no answer arrived. + async fn exchange(&self, request: ApiRequest) -> Result; +} + +/// The bytes of one pinned source version, read at offsets. +#[allow(async_fn_in_trait)] +pub trait SourceReader { + /// The version's size. + fn size(&self) -> u64; + + /// Fills `destination` from `offset`. + /// + /// # Errors + /// + /// A [`Failure`] where the read fails or the version changed. + async fn read_exact_at(&self, offset: u64, destination: &mut [u8]) -> Result<(), Failure>; +} + +impl SourceReader for [u8] { + fn size(&self) -> u64 { + u64::try_from(self.len()).unwrap_or(u64::MAX) + } + + async fn read_exact_at(&self, offset: u64, destination: &mut [u8]) -> Result<(), Failure> { + let start = + usize::try_from(offset).map_err(|_| Failure::integrity("read_out_of_bounds"))?; + let end = start + .checked_add(destination.len()) + .filter(|end| *end <= self.len()) + .ok_or_else(|| Failure::integrity("read_out_of_bounds"))?; + destination.copy_from_slice(&self[start..end]); + Ok(()) + } +} + +/// A [`SourceReader`] as the probe reads it. +struct Probed<'a, S: ?Sized>(&'a S); + +impl ProbeSource for Probed<'_, S> { + type Error = Failure; + + fn size(&self) -> u64 { + self.0.size() + } + + async fn read_exact_at(&self, offset: u64, destination: &mut [u8]) -> Result<(), Failure> { + self.0.read_exact_at(offset, destination).await + } +} + +fn probe_failure>(op: &str, error: ProbeCallError) -> Failure { + log::info!("encypher preparation: probe op={op} code={}", error.code()); + match error { + ProbeCallError::Source(error) => error.into(), + ProbeCallError::Refused(refusal) if refusal.integrity => { + Failure::integrity(&refusal.code.to_ascii_lowercase()) + } + ProbeCallError::Refused(refusal) => { + Failure::unsupported(&refusal.code.to_ascii_lowercase()) + } + ProbeCallError::Failed(_) => Failure::transient("probe_failed"), + } +} + +impl From for Failure { + fn from(never: Infallible) -> Self { + match never {} + } +} + +/// The failure an error answer of the API names. +fn answer_failure(status: u16, body: &[u8]) -> Failure { + let code = crate::media_api::error_code(body); + let code = code.as_deref(); + match (status, code) { + (409, Some("TRUSTED_SERVER_POLICY_CHANGED")) => { + Failure::new(FailureClass::PolicyChanged, "policy_changed") + } + (409, Some("PROBE_VERSION_UNSUPPORTED" | "PLAN_STALE")) => { + Failure::transient("probe_release_stale") + } + (409, Some("SOURCE_DIGEST_MISMATCH")) => Failure::integrity("source_digest_mismatch"), + (422, Some("SOURCE_PROVENANCE_INVALID")) => Failure::integrity("source_provenance_invalid"), + (422, Some("SOURCE_MANIFEST_PRESENT")) => Failure::new( + FailureClass::ExistingProvenance, + "source_has_embedded_provenance", + ), + (422, Some("HASH_MODE_UNSUPPORTED")) => Failure::unsupported("format_unsupported"), + (404, _) => Failure::new(FailureClass::Missing, "result_not_retained"), + (410, _) => Failure::integrity("result_retired"), + (403, _) => Failure::new(FailureClass::Privacy, "forbidden"), + (409, _) => Failure::integrity("prepared_conflict"), + (413, _) => Failure::unsupported("payload_too_large"), + (422, _) => Failure::unsupported("unprocessable"), + (429, _) => Failure::transient("rate_limited"), + (500..=599, _) => Failure::transient("server_error"), + _ => Failure::transient("unexpected_status"), + } +} + +async fn exchange_ok( + transport: &T, + request: ApiRequest, +) -> Result { + let limit = request.max_response_bytes; + let answer = transport.exchange(request).await?; + if answer.status != 200 { + return Err(answer_failure(answer.status, &answer.body)); + } + if answer.body.len() > limit { + return Err(Failure::transient("response_too_large")); + } + Ok(answer.body) +} + +async fn post_json( + transport: &T, + path: &str, + body: &impl Serialize, + idempotency_key: &str, + max_response_bytes: usize, +) -> Result { + let body = serde_json::to_vec(body).map_err(|_| Failure::integrity("request_build"))?; + let answer = exchange_ok( + transport, + ApiRequest { + method: Method::POST, + path_and_query: path.to_owned(), + body: Some(body), + idempotency_key: Some(idempotency_key.to_owned()), + credentialed: true, + max_response_bytes, + }, + ) + .await?; + serde_json::from_slice(&answer).map_err(|_| Failure::transient("malformed_response")) +} + +fn path_with_page_url(path: &str, page_url: &str) -> String { + let mut query = url::form_urlencoded::Serializer::new(String::new()); + query.append_pair("page_url", page_url); + format!("{path}?{}", query.finish()) +} + +/// The policy the API holds now for `page_url`. +/// +/// # Errors +/// +/// A [`Failure`] for any answer other than a valid digest. +pub async fn policy_digest( + transport: &T, + page_url: &str, +) -> Result { + let body = exchange_ok( + transport, + ApiRequest { + method: Method::GET, + path_and_query: path_with_page_url(POLICY_PATH, page_url), + body: None, + idempotency_key: None, + credentialed: true, + max_response_bytes: MAX_POLICY_BYTES, + }, + ) + .await?; + api::parse_policy_response(&body).map_err(|_| Failure::transient("malformed_policy")) +} + +/// What the prepared lane signs for a host, as the API answers it. +#[derive(Debug, Clone)] +pub struct Capabilities { + /// The policy the answer was made under. + pub policy_digest: String, + /// Formats signed with an external manifest. + pub formats: Vec, + /// Formats signed with an embedded manifest under contract version 2. + pub embedded_formats: Vec, + /// Worker contract versions the API speaks. + pub contract_versions: Vec, + /// The largest source. + pub total_size_bytes: u64, + /// The largest decoded manifest store. + pub manifest_decoded_bytes: usize, + probes: Vec, +} + +impl Capabilities { + fn parse(body: &[u8]) -> Option { + let value: Value = serde_json::from_slice(body).ok()?; + if value.get("protocol")?.as_str()? != PREPARED_PROTOCOL + || !value.get("enabled")?.as_bool()? + { + return None; + } + let strings = |name: &str| -> Option> { + Some(match value.get(name) { + None => Vec::new(), + Some(list) => list + .as_array()? + .iter() + .filter_map(Value::as_str) + .map(str::to_owned) + .collect(), + }) + }; + let maximums = value.get("maximums")?; + Some(Self { + policy_digest: value.get("policy_digest")?.as_str()?.to_owned(), + formats: strings("formats")?, + embedded_formats: strings("embedded_formats")?, + contract_versions: match value.get("contract_versions") { + None => vec![1], + Some(list) => list + .as_array()? + .iter() + .filter_map(Value::as_u64) + .filter_map(|version| u8::try_from(version).ok()) + .collect(), + }, + total_size_bytes: maximums.get("total_size_bytes")?.as_u64()?, + manifest_decoded_bytes: usize::try_from( + maximums.get("manifest_decoded_bytes")?.as_u64()?, + ) + .ok()?, + probes: ProbeDescriptor::parse_list(value.get("probe")), + }) + } + + /// Whether the API signs `mime` with `carrier` under this pipeline's + /// contract version. Decided by what the engine advertises, never by a + /// list of the edge's own. + #[must_use] + pub fn supports(&self, mime: &str, carrier: Carrier) -> bool { + self.contract_versions.contains(&CONTRACT_VERSION) + && match carrier { + Carrier::External => self.formats.iter().any(|format| format == mime), + Carrier::Embedded => self.embedded_formats.iter().any(|format| format == mime), + } + } +} + +/// The prepared lane's capabilities for `page_url`. +/// +/// # Errors +/// +/// A [`Failure`] for any answer other than enabled capabilities. +pub async fn capabilities( + transport: &T, + page_url: &str, +) -> Result { + let body = exchange_ok( + transport, + ApiRequest { + method: Method::GET, + path_and_query: path_with_page_url(CAPABILITIES_PATH, page_url), + body: None, + idempotency_key: None, + credentialed: true, + max_response_bytes: MAX_CAPABILITIES_BYTES, + }, + ) + .await?; + Capabilities::parse(&body).ok_or_else(|| Failure::unsupported("capabilities_unavailable")) +} + +/// A signed probe release descriptor, as a reconstruction record keeps it so +/// recovery runs exactly the release a plan was verified with. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ProbeRelease { + /// The probe ABI. + pub abi: u64, + /// The planner. + pub planner_id: String, + /// The release number. + pub release: u64, + /// The artifact SHA-256. + pub sha256: String, + /// The release key's signature over the other fields. + pub signature: String, +} + +impl ProbeRelease { + fn descriptor(&self) -> ProbeDescriptor { + ProbeDescriptor { + abi: self.abi, + planner_id: self.planner_id.clone(), + release: self.release, + sha256: self.sha256.clone(), + signature: self.signature.clone(), + } + } +} + +/// A verified, compiled probe release. +#[derive(Debug)] +pub struct LoadedProbe(ProbeModule); + +impl LoadedProbe { + /// The release's artifact SHA-256 and planner, which reconstruction + /// records bind. + #[must_use] + pub fn identity(&self) -> (&str, &str) { + let descriptor = self.0.descriptor(); + (&descriptor.sha256, &descriptor.planner_id) + } + + /// The signed descriptor of the release, which a reconstruction record + /// keeps. + #[must_use] + pub fn release(&self) -> ProbeRelease { + let descriptor = self.0.descriptor(); + ProbeRelease { + abi: descriptor.abi, + planner_id: descriptor.planner_id.clone(), + release: descriptor.release, + sha256: descriptor.sha256.clone(), + signature: descriptor.signature.clone(), + } + } +} + +/// Where verified probe artifacts are kept between loads. +#[allow(async_fn_in_trait)] +pub trait ProbeArtifactCache { + /// The artifact with SHA-256 `sha256`, if kept. + async fn get(&self, sha256: &str) -> Option; + /// Keeps `artifact`, whose SHA-256 is `sha256`. + /// + /// # Errors + /// + /// A [`Failure`] where the artifact may not be kept, such as a store + /// whose byte quota refuses it. The load then fails before the release + /// plans, finalizes or reconstructs anything, so nothing comes to + /// depend on an artifact that was never retained. + async fn put(&self, sha256: &str, artifact: Bytes) -> Result<(), Failure>; +} + +/// The authenticated advertised artifact and release, without loading its WASM. +/// +/// # Errors +/// +/// A [`Failure`] when the newest compatible descriptor fails production trust. +pub fn advertised_release_identity(capabilities: &Capabilities) -> Result<(&str, u64), Failure> { + let authority = ReleaseAuthority::production(); + let descriptor = authority + .select(&capabilities.probes) + .map_err(|error| Failure::unsupported(&error.code().to_ascii_lowercase()))?; + Ok((descriptor.sha256.as_str(), descriptor.release)) +} + +/// The newest release the capabilities advertise that Encypher's pinned +/// production key signed, loaded from `cache` or fetched without +/// credentials and verified before it runs. +/// +/// # Errors +/// +/// A [`Failure`] where no advertised release verifies. +pub async fn load_probe( + transport: &T, + cache: &C, + capabilities: &Capabilities, +) -> Result { + let authority = ReleaseAuthority::production(); + let descriptor = authority + .select(&capabilities.probes) + .map_err(|error| Failure::unsupported(&error.code().to_ascii_lowercase()))?; + load_descriptor(transport, cache, descriptor, &authority).await +} + +/// Exactly `release`, for recovering a completed receipt: verified against +/// Encypher's pinned production key, floor and revocations before it runs, +/// never replaced by a newer release, and refused explicitly where it is no +/// longer allowed to run. Needs no capabilities, so recovery works while +/// new preparation is closed. +/// +/// # Errors +/// +/// A [`Failure`] where the release does not verify or cannot be fetched. +pub async fn load_release( + transport: &T, + cache: &C, + release: &ProbeRelease, +) -> Result { + load_descriptor( + transport, + cache, + &release.descriptor(), + &ReleaseAuthority::production(), + ) + .await +} + +async fn load_descriptor( + transport: &T, + cache: &C, + descriptor: &ProbeDescriptor, + authority: &ReleaseAuthority, +) -> Result { + authority + .verify(descriptor) + .map_err(|error| Failure::integrity(&error.code().to_ascii_lowercase()))?; + let artifact = match cache.get(&descriptor.sha256).await { + Some(bytes) if sha256_hex(&bytes) == descriptor.sha256 => bytes, + _ => { + let bytes = exchange_ok( + transport, + ApiRequest { + method: Method::GET, + path_and_query: format!("{PROBE_PATH}{}", descriptor.artifact_path()), + body: None, + idempotency_key: None, + credentialed: false, + max_response_bytes: MAX_PROBE_BYTES, + }, + ) + .await?; + if sha256_hex(&bytes) != descriptor.sha256 { + return Err(Failure::integrity("probe_release_invalid")); + } + cache.put(&descriptor.sha256, bytes.clone()).await?; + bytes + } + }; + ProbeModule::load(authority, descriptor, &artifact, ProbeLimits::default()) + .await + .map(LoadedProbe) + .map_err(|error| Failure::unsupported(&error.code().to_ascii_lowercase())) +} + +/// The API cache partition required by prepared text operations. +/// +/// A closed scope keeps old or unsupported requests from using the legacy +/// content-and-rights cache during signing, recovery or result publication. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TextCacheScope { + /// Reuse only a result for the exact canonical sign request. + ExactRequest, +} + +/// The exact text sign request, as sent and as recovered. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TextSignRequest { + /// Canonical visible article text. + pub text: String, + /// Absolute page URL without its query. + pub page_url: String, + /// Optional page title. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub document_title: Option, + /// The article boundary selector. + pub boundary_selector: String, + /// The policy the request is frozen under. + pub expected_policy_digest: String, + /// Required exact-request cache isolation, including on recovery. + pub cache_scope: TextCacheScope, +} + +/// A text sign result. +#[derive(Debug, Clone, PartialEq)] +pub struct TextSigned { + /// The marker plan. + pub plan: EmbeddingPlan, + /// The manifest URL, if any. + pub manifest_url: Option, +} + +fn text_result(body: &[u8], request: &TextSignRequest) -> Result { + let parsed = + api::parse_sign_response(body).map_err(|_| Failure::transient("malformed_response"))?; + if !parsed.success { + return Err(if parsed.error.as_deref() == Some("quota_exceeded") { + Failure::transient("quota_exceeded") + } else { + Failure::unsupported("sign_rejected") + }); + } + if parsed.policy_digest.as_deref() != Some(request.expected_policy_digest.as_str()) { + return Err(Failure::new( + FailureClass::PolicyChanged, + "policy_digest_mismatch", + )); + } + let plan = parsed + .embedding_plan + .ok_or_else(|| Failure::integrity("no_plan"))?; + let scalars = request.text.chars().count(); + api::validate_plan(&plan, parsed.manifest_url.as_deref(), scalars) + .map_err(|_| Failure::integrity("invalid_plan"))?; + Ok(TextSigned { + plan, + manifest_url: parsed.manifest_url, + }) +} + +async fn text_call( + transport: &T, + path: &str, + request: &TextSignRequest, + idempotency_key: &str, +) -> Result { + let body = serde_json::to_vec(request).map_err(|_| Failure::integrity("request_build"))?; + let answer = exchange_ok( + transport, + ApiRequest { + method: Method::POST, + path_and_query: path.to_owned(), + body: Some(body), + idempotency_key: Some(idempotency_key.to_owned()), + credentialed: true, + max_response_bytes: api::MAX_RESPONSE_BYTES, + }, + ) + .await?; + text_result(&answer, request) +} + +/// Signs an article's text once under `idempotency_key`. Only the worker +/// calls this; a reader never does. +/// +/// # Errors +/// +/// A [`Failure`] for any answer other than a valid plan under the request's +/// policy. +pub async fn sign_text( + transport: &T, + request: &TextSignRequest, + idempotency_key: &str, +) -> Result { + text_call(transport, SIGN_PATH, request, idempotency_key).await +} + +/// Recovers the exact completed text result for `request` and +/// `idempotency_key`, without signing. +/// +/// # Errors +/// +/// A [`FailureClass::Missing`] failure where the API holds none. +pub async fn recover_text( + transport: &T, + request: &TextSignRequest, + idempotency_key: &str, +) -> Result { + text_call(transport, SIGN_RECOVER_PATH, request, idempotency_key).await +} + +/// The prepared source a prepare request names. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PreparedSourceRef { + /// The public address of the exact source bytes. + pub reference: String, + /// The host the source was fetched from. + pub origin_host: String, + /// The canonical media type. + pub mime: String, + /// SHA-256 of every source byte. + pub sha256: String, + /// The source length. + pub byte_length: u64, + /// `origin` or `publisher_snapshot`. + pub reference_kind: String, +} + +/// The probe release a prepare request selects. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ProbeSelection { + /// The artifact SHA-256. + pub sha256: String, + /// The planner. + pub planner_id: String, +} + +/// One disclosed structure chunk. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct StructureChunk { + /// Its source offset. + pub offset: u64, + /// Its bytes, base64. + pub b64: String, +} + +/// The exact contract version 2 prepare request, as sent and as recovered. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PrepareRequest { + /// [`CONTRACT_VERSION`]. + pub contract_version: u8, + /// `external_allowed` or `embedded_required`. + pub carrier_requirement: String, + /// The reader-facing address of the item. + pub page_url: String, + /// The policy frozen into the plan. + pub expected_policy_digest: String, + /// [`PREPARED_PROTOCOL`]. + pub protocol: String, + /// The probe release. + pub probe: ProbeSelection, + /// The source. + pub source: PreparedSourceRef, + /// The structure disclosure. + pub structure: Vec, +} + +impl PrepareRequest { + /// The carrier the request requires. + #[must_use] + pub fn carrier(&self) -> Carrier { + if self.carrier_requirement == "embedded_required" { + Carrier::Embedded + } else { + Carrier::External + } + } + + fn disclosure(&self) -> Value { + Value::Array( + self.structure + .iter() + .map(|chunk| json!({"offset": chunk.offset, "data": chunk.b64})) + .collect(), + ) + } +} + +/// What one media item is prepared from. +#[derive(Debug, Clone)] +pub struct MediaJob<'a> { + /// The reader-facing address of the item. + pub page_url: &'a str, + /// The policy it is prepared under. + pub policy_digest: &'a str, + /// The carrier required. + pub carrier: Carrier, + /// The canonical media type. + pub mime: &'a str, + /// SHA-256 of every source byte, computed by the caller as it captured + /// the source. + pub source_sha256: &'a str, + /// The public reference, its host and its kind. + pub reference: &'a str, + /// The host the source was fetched from. + pub origin_host: &'a str, + /// `origin` or `publisher_snapshot`. + pub reference_kind: &'a str, + /// The idempotency key prepare and finalize send. + pub idempotency_key: &'a str, +} + +/// The external extract of `source`, refusing a source with provenance of +/// its own. It alone reports whether the XMP names a manifest, which the +/// embedded planner cannot see. +async fn inspect_provenance( + probe: &LoadedProbe, + source: &S, + mime: &str, +) -> Result { + let inspected = probe + .0 + .call( + "extract", + &json!({"mime": mime, "total_size": source.size(), "disposition": "external"}), + &Probed(source), + ) + .await + .map_err(|error| probe_failure("extract", error))?; + // An embedded manifest wins over a new one, and a reference in the XMP + // is provenance of its own: either way the source keeps it. + if inspected.get("credentials").is_some() { + return Err(Failure::new( + FailureClass::ExistingProvenance, + "source_has_embedded_provenance", + )); + } + match inspected + .get("has_provenance_reference") + .and_then(Value::as_bool) + { + Some(true) => Err(Failure::new( + FailureClass::ExistingProvenance, + "source_has_provenance_reference", + )), + Some(false) => Ok(inspected), + None => Err(Failure::integrity("probe_result_invalid")), + } +} + +/// Refuses a disclosure that is not the source's own bytes. Every chunk is +/// non-empty, in order, without overlap, within the contract's ceilings and +/// byte for byte `source` at its offset. The probe takes a disclosure as +/// the parse of the source and never reads a disclosed byte again, so a +/// chunk the source does not hold would plan the output from structure the +/// source does not have. +fn check_structure(structure: &[StructureChunk], source: &[u8]) -> Result<(), Failure> { + let mismatch = || Failure::integrity("structure_mismatch"); + if structure.is_empty() || structure.len() > MAX_STRUCTURE_CHUNKS { + return Err(mismatch()); + } + let mut decoded = Vec::new(); + let mut total = 0_usize; + let mut end = 0_usize; + for chunk in structure { + // Three bytes per four characters bounds the decode before it + // allocates. + if chunk.b64.len() / 4 * 3 > MAX_STRUCTURE_BYTES - total { + return Err(mismatch()); + } + decoded.clear(); + STANDARD + .decode_vec(&chunk.b64, &mut decoded) + .map_err(|_| mismatch())?; + total += decoded.len(); + let start = usize::try_from(chunk.offset).map_err(|_| mismatch())?; + let chunk_end = start + .checked_add(decoded.len()) + .filter(|chunk_end| *chunk_end <= source.len()) + .ok_or_else(mismatch)?; + if decoded.is_empty() || start < end || source[start..chunk_end] != decoded[..] { + return Err(mismatch()); + } + end = chunk_end; + } + Ok(()) +} + +/// The extract step's disclosure, or the failure that keeps the source as +/// it is. +/// +/// The provenance inspection, [`inspect_provenance`], runs first for every +/// carrier. A source with provenance of either kind is kept, never +/// overwritten. An embedded item's disclosure then comes from the embedded +/// extract. +async fn extract( + probe: &LoadedProbe, + source: &S, + mime: &str, + carrier: Carrier, +) -> Result, Failure> { + let inspected = inspect_provenance(probe, source, mime).await?; + let extracted = match carrier { + Carrier::External => inspected, + Carrier::Embedded => { + let embedded = probe + .0 + .call( + "extract", + &json!({"mime": mime, "total_size": source.size()}), + &Probed(source), + ) + .await + .map_err(|error| probe_failure("extract", error))?; + if embedded.get("credentials").is_some() { + return Err(Failure::new( + FailureClass::ExistingProvenance, + "source_has_embedded_provenance", + )); + } + embedded + } + }; + extracted + .get("chunks") + .and_then(Value::as_array) + .ok_or_else(|| Failure::integrity("probe_result_invalid"))? + .iter() + .map(|chunk| { + Some(StructureChunk { + offset: chunk.get("offset")?.as_u64()?, + b64: chunk.get("data")?.as_str()?.to_owned(), + }) + }) + .collect::>>() + .ok_or_else(|| Failure::integrity("probe_result_invalid")) +} + +/// Runs a verified hash program over `source`: every range, then every +/// literal, in order, into one SHA-256. +async fn run_hash_program( + items: &Value, + source: &S, +) -> Result { + const CHUNK: u64 = 1024 * 1024; + let mut hasher = Sha256::new(); + let mut buffer = Vec::new(); + for item in items + .as_array() + .ok_or_else(|| Failure::integrity("hash_program_invalid"))? + { + if let Some(range) = item.get("range").and_then(Value::as_array) { + let (Some(offset), Some(length)) = ( + range.first().and_then(Value::as_u64), + range.get(1).and_then(Value::as_u64), + ) else { + return Err(Failure::integrity("hash_program_invalid")); + }; + let end = offset + .checked_add(length) + .filter(|end| *end <= source.size()) + .ok_or_else(|| Failure::integrity("hash_program_out_of_bounds"))?; + let mut at = offset; + while at < end { + let take = (end - at).min(CHUNK); + buffer.resize(usize::try_from(take).unwrap_or(0), 0); + source.read_exact_at(at, &mut buffer).await?; + hasher.update(&buffer); + at += take; + } + } else if let Some(literal) = item.get("literal").and_then(Value::as_str) { + let bytes = STANDARD + .decode(literal) + .map_err(|_| Failure::integrity("hash_program_invalid"))?; + hasher.update(&bytes); + } else { + return Err(Failure::integrity("hash_program_invalid")); + } + } + Ok(hex_lower(&hasher.finalize())) +} + +/// Whether `items` is the one program of an unchanged external plan: the +/// whole source, once, with nothing excluded or inserted. +fn covers_whole_source(items: &Value, size: u64) -> bool { + matches!(items.as_array().map(Vec::as_slice), Some([item]) if item.get("range").and_then(Value::as_array).is_some_and(|range| { + range.len() == 2 + && range.first().and_then(Value::as_u64) == Some(0) + && range.get(1).and_then(Value::as_u64) == Some(size) + }) && item.as_object().is_some_and(|object| object.len() == 1)) +} + +/// Writes `source` through the verified edits of `geometry` with the +/// locally built `carrier`, refusing any edit out of order, beyond the +/// source, malformed or over its bound, and any output whose size is not +/// the plan's. +fn compose(source: &[u8], geometry: &Value, carrier: &[u8]) -> Result, Failure> { + let mismatch = || Failure::integrity("plan_mismatch"); + let rows = geometry + .get("edits") + .and_then(Value::as_array) + .ok_or_else(mismatch)?; + let source_length = u64::try_from(source.len()).unwrap_or(u64::MAX); + let expected = geometry + .get("expected_final_size") + .and_then(Value::as_u64) + .filter(|size| *size <= output_ceiling(source_length)) + .and_then(|size| usize::try_from(size).ok()) + .ok_or_else(mismatch)?; + let mut output = Vec::with_capacity(expected); + let mut cursor = 0_usize; + for row in rows { + let offset = row + .get("offset") + .and_then(Value::as_u64) + .and_then(|offset| usize::try_from(offset).ok()) + .filter(|offset| *offset >= cursor && *offset <= source.len()) + .ok_or_else(mismatch)?; + output.extend_from_slice(&source[cursor..offset]); + cursor = offset; + let decode = |row: &Value| { + row.get("b64") + .and_then(Value::as_str) + .and_then(|text| STANDARD.decode(text).ok()) + }; + match row.get("op").and_then(Value::as_str) { + Some("delete") => { + let length = row + .get("length") + .and_then(Value::as_u64) + .and_then(|length| usize::try_from(length).ok()) + .ok_or_else(mismatch)?; + cursor = cursor + .checked_add(length) + .filter(|end| *end <= source.len()) + .ok_or_else(mismatch)?; + } + Some("patch") => { + let bytes = decode(row).ok_or_else(mismatch)?; + cursor = cursor + .checked_add(bytes.len()) + .filter(|end| *end <= source.len()) + .ok_or_else(mismatch)?; + output.extend_from_slice(&bytes); + } + Some("insert_carrier") => { + let length = row.get("length").and_then(Value::as_u64); + if length != Some(u64::try_from(carrier.len()).unwrap_or(u64::MAX)) { + return Err(Failure::integrity("carrier_mismatch")); + } + output.extend_from_slice(carrier); + } + Some("insert_bytes") => { + let bytes = decode(row) + .filter(|bytes| !bytes.is_empty() && bytes.len() <= MAX_INSERT_BYTES) + .ok_or_else(mismatch)?; + output.extend_from_slice(&bytes); + } + _ => return Err(mismatch()), + } + if output.len() > expected { + return Err(Failure::integrity("output_verification_failed")); + } + } + output.extend_from_slice(&source[cursor..]); + if output.len() != expected { + return Err(Failure::integrity("output_verification_failed")); + } + Ok(output) +} + +fn text_field<'a>(value: &'a Value, name: &str) -> Option<&'a str> { + value.get(name).and_then(Value::as_str) +} + +/// Whether `value` is an identifier safe in a response header. +fn valid_identifier(value: &str) -> bool { + !value.is_empty() && value.len() <= 256 && value.bytes().all(|byte| byte.is_ascii_graphic()) +} + +/// Whether `value` is a canonical absolute `https` URL safe inside a `Link`. +fn valid_https_url(value: &str) -> bool { + value.len() <= 2048 + && value + .bytes() + .all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'<' | b'>' | b'"')) + && url::Url::parse(value).is_ok_and(|parsed| { + parsed.scheme() == "https" && parsed.host_str().is_some() && parsed.as_str() == value + }) +} + +/// The identifier headers a finalize answer names. +fn identity_headers(finalized: &Value) -> Result, Failure> { + let instance_id = text_field(finalized, "instance_id") + .filter(|id| valid_identifier(id)) + .ok_or_else(|| Failure::integrity("invalid_signed_response"))?; + let document_id = text_field(finalized, "document_id") + .filter(|id| valid_identifier(id)) + .ok_or_else(|| Failure::integrity("invalid_signed_response"))?; + let mut headers = vec![ + ( + "x-encypher-manifest-instance-id".to_owned(), + instance_id.to_owned(), + ), + ("x-encypher-document-id".to_owned(), document_id.to_owned()), + ]; + if let Some(url) = text_field(finalized, "verification_url") { + if !valid_https_url(url) { + return Err(Failure::integrity("invalid_signed_response")); + } + headers.push(("x-encypher-verification-url".to_owned(), url.to_owned())); + } + Ok(headers) +} + +/// The decoded manifest store a finalize answer carries, checked against +/// its stated digest and `limit`. +fn finalized_store(finalized: &Value, limit: usize) -> Result<(String, Vec), Failure> { + let store_b64 = text_field(finalized, "manifest_store_b64") + .ok_or_else(|| Failure::integrity("unsigned_response"))?; + if store_b64.len() > limit.saturating_mul(4).div_ceil(3).saturating_add(3) { + return Err(Failure::unsupported("manifest_too_large")); + } + let store = STANDARD + .decode(store_b64) + .map_err(|_| Failure::integrity("invalid_signed_response"))?; + if store.is_empty() || store.len() > limit { + return Err(Failure::integrity("invalid_signed_response")); + } + if text_field(finalized, "manifest_sha256") != Some(sha256_hex(&store).as_str()) { + return Err(Failure::integrity("signed_digest_mismatch")); + } + Ok((store_b64.to_owned(), store)) +} + +async fn check_store( + probe: &LoadedProbe, + store_b64: &str, + digest: &str, + recipe: &Value, +) -> Result<(), Failure> { + let checked = probe + .0 + .call_without_source::( + "check_store", + &json!({"manifest_store_b64": store_b64, "digest": digest, "binding_recipe": recipe}), + ) + .await + .map_err(|error| probe_failure("check_store", error))?; + if checked.get("ok").and_then(Value::as_bool) == Some(false) { + return Err(Failure::integrity("store_mismatch")); + } + Ok(()) +} + +fn prepare_request( + job: &MediaJob<'_>, + probe: &LoadedProbe, + size: u64, + structure: Vec, +) -> PrepareRequest { + let (sha256, planner_id) = probe.identity(); + PrepareRequest { + contract_version: CONTRACT_VERSION, + carrier_requirement: match job.carrier { + Carrier::External => "external_allowed", + Carrier::Embedded => "embedded_required", + } + .to_owned(), + page_url: job.page_url.to_owned(), + expected_policy_digest: job.policy_digest.to_owned(), + protocol: PREPARED_PROTOCOL.to_owned(), + probe: ProbeSelection { + sha256: sha256.to_owned(), + planner_id: planner_id.to_owned(), + }, + source: PreparedSourceRef { + reference: job.reference.to_owned(), + origin_host: job.origin_host.to_owned(), + mime: job.mime.to_owned(), + sha256: job.source_sha256.to_owned(), + byte_length: size, + reference_kind: job.reference_kind.to_owned(), + }, + structure, + } +} + +/// The scalar binding type a verified plan finalizes with: `data_hash` for +/// every external plan, whose manifest binds the whole unchanged source, +/// and the type of the embedded plan's own recipe otherwise, which the +/// server chose from the format (`bmff_hash` for ISO BMFF, `data_hash` for +/// the rest). Any other recipe, such as a Merkle or collection binding, is +/// not a scalar this lane finalizes. +fn binding_type(request: &PrepareRequest, geometry: &Value) -> Result<&'static str, Failure> { + match request.carrier() { + Carrier::External => Ok("data_hash"), + Carrier::Embedded => match geometry + .get("binding_recipe") + .and_then(|recipe| text_field(recipe, "type")) + { + Some("data_hash") => Ok("data_hash"), + Some("bmff_hash") => Ok("bmff_hash"), + _ => Err(Failure::unsupported("binding_recipe_unsupported")), + }, + } +} + +fn finalize_body(planned: &PlannedMedia) -> Result { + let request = &planned.request; + let binding = binding_type(request, &planned.geometry)?; + Ok(json!({ + "contract_version": request.contract_version, + "carrier_requirement": request.carrier_requirement, + "page_url": request.page_url, + "expected_policy_digest": request.expected_policy_digest, + "protocol": PREPARED_PROTOCOL, + "plan_id": planned.plan_id, + "binding": {"type": binding, "digest": planned.digest}, + "xmp_reference": false, + })) +} + +fn checked_plan(prepared: &Value, request: &PrepareRequest) -> Result<(String, Value), Failure> { + let plan_id = text_field(prepared, "plan_id") + .filter(|id| id.len() == 36) + .ok_or_else(|| Failure::integrity("prepare_answer_invalid"))? + .to_owned(); + if text_field(prepared, "policy_digest") != Some(request.expected_policy_digest.as_str()) { + return Err(Failure::new( + FailureClass::PolicyChanged, + "policy_digest_mismatch", + )); + } + let geometry = prepared + .get("geometry") + .cloned() + .ok_or_else(|| Failure::integrity("prepare_answer_invalid"))?; + let disposition = text_field(&geometry, "disposition"); + match request.carrier() { + Carrier::External => { + let unchanged = prepared + .get("local_ops") + .and_then(Value::as_array) + .is_some_and(Vec::is_empty) + && text_field(prepared, "effective_action") == Some("c2pa.opened"); + if disposition != Some("external") || !unchanged { + return Err(Failure::integrity("prepare_answer_invalid")); + } + } + Carrier::Embedded => { + if disposition != Some("embedded") { + return Err(Failure::unsupported("embedded_plan_unavailable")); + } + binding_type(request, &geometry)?; + let within = geometry + .get("expected_final_size") + .and_then(Value::as_u64) + .is_some_and(|size| size <= output_ceiling(request.source.byte_length)); + if !within { + return Err(Failure::unsupported("output_too_large")); + } + } + } + Ok((plan_id, geometry)) +} + +/// The digest a verified plan's hash program yields over `source`, after +/// the probe recomputed the server's plan from the same bytes. +async fn verified_digest( + probe: &LoadedProbe, + request: &PrepareRequest, + geometry: &Value, + source: &S, +) -> Result { + let verified = probe + .0 + .call( + "verify_plan", + &json!({ + "mime": request.source.mime, + "total_size": source.size(), + "disclosure": request.disclosure(), + "geometry": geometry, + }), + &Probed(source), + ) + .await + .map_err(|error| probe_failure("verify_plan", error))?; + let items = verified + .get("hash_items") + .ok_or_else(|| Failure::integrity("probe_result_invalid"))?; + if request.carrier() == Carrier::External { + if !covers_whole_source(items, source.size()) { + return Err(Failure::integrity("plan_not_whole_source")); + } + // The whole source, once: its digest is the one the caller + // computed while it captured every byte. + return Ok(request.source.sha256.clone()); + } + run_hash_program(items, source).await +} + +/// Whether a finalize answer's scalar binding is exactly `kind` over +/// `digest`. +fn binding_names(finalized: &Value, kind: &str, digest: &str) -> bool { + finalized.get("binding").is_some_and(|binding| { + text_field(binding, "type") == Some(kind) && text_field(binding, "digest") == Some(digest) + }) +} + +/// The external result a finalize answer supports for `request`. +async fn external_result( + probe: &LoadedProbe, + request: &PrepareRequest, + finalized: &Value, + manifest_limit: usize, +) -> Result { + if text_field(finalized, "policy_digest") != Some(request.expected_policy_digest.as_str()) { + return Err(Failure::new( + FailureClass::PolicyChanged, + "policy_digest_mismatch", + )); + } + let external = text_field(finalized, "manifest_disposition") == Some("external") + && text_field(finalized, "binding_algorithm") == Some("c2pa.hash.data") + && finalized.get("expected_final_size").and_then(Value::as_u64) + == Some(request.source.byte_length) + && binding_names(finalized, "data_hash", &request.source.sha256); + if !external { + return Err(Failure::integrity("finalize_answer_invalid")); + } + let (store_b64, _) = finalized_store(finalized, manifest_limit)?; + check_store( + probe, + &store_b64, + &request.source.sha256, + &json!({"type": "data_hash", "algorithm": "sha256", "exclusions": []}), + ) + .await?; + let manifest_url = text_field(finalized, "manifest_url") + .filter(|url| valid_https_url(url)) + .ok_or_else(|| Failure::integrity("invalid_signed_response"))?; + let mut headers = vec![( + "link".to_owned(), + format!("<{manifest_url}>; rel=\"c2pa-manifest\""), + )]; + headers.extend(identity_headers(finalized)?); + if !headers + .iter() + .any(|(name, _)| name == "x-encypher-verification-url") + { + return Err(Failure::integrity("invalid_signed_response")); + } + if request.source.byte_length > u64::try_from(usize::MAX).unwrap_or(u64::MAX) { + return Err(Failure::integrity("source_too_large")); + } + Ok(PublishedResult::External { + source_sha256: request.source.sha256.clone(), + headers, + }) +} + +/// The embedded output a finalize answer supports for `request` over +/// `source`, built and walked here, and the result a reader applies. +async fn embedded_output( + probe: &LoadedProbe, + request: &PrepareRequest, + geometry: &Value, + digest: &str, + finalized: &Value, + source: &[u8], + manifest_limit: usize, +) -> Result<(Vec, PublishedResult), Failure> { + if text_field(finalized, "policy_digest") != Some(request.expected_policy_digest.as_str()) { + return Err(Failure::new( + FailureClass::PolicyChanged, + "policy_digest_mismatch", + )); + } + let bound = binding_names(finalized, binding_type(request, geometry)?, digest) + && finalized.get("expected_final_size") == geometry.get("expected_final_size"); + if !bound { + return Err(Failure::integrity("finalize_answer_invalid")); + } + let (store_b64, _) = finalized_store(finalized, manifest_limit)?; + let carrier_sha256 = text_field(finalized, "carrier_sha256") + .filter(|sha| super::protocol::is_digest(sha)) + .ok_or_else(|| Failure::integrity("invalid_signed_response"))?; + let built = probe + .0 + .call_without_source::( + "build_carrier", + &json!({ + "mime": request.source.mime, + "geometry": geometry, + "manifest_store_b64": store_b64, + "carrier_sha256": carrier_sha256, + }), + ) + .await + .map_err(|error| probe_failure("build_carrier", error))?; + let carrier_b64 = text_field(&built, "carrier_b64") + .ok_or_else(|| Failure::integrity("probe_result_invalid"))?; + let carrier = STANDARD + .decode(carrier_b64) + .map_err(|_| Failure::integrity("probe_result_invalid"))?; + if sha256_hex(&carrier) != carrier_sha256 { + return Err(Failure::integrity("carrier_mismatch")); + } + let output = compose(source, geometry, &carrier)?; + let walked = probe + .0 + .call( + "verify_output", + &json!({ + "total_size": output.len(), + "mime": request.source.mime, + "geometry": geometry, + "carrier_b64": carrier_b64, + }), + &Probed(output.as_slice()), + ) + .await + .map_err(|error| probe_failure("verify_output", error))?; + let items = walked + .get("hash_items") + .ok_or_else(|| Failure::integrity("probe_result_invalid"))?; + if run_hash_program(items, output.as_slice()).await? != digest { + return Err(Failure::integrity("output_verification_failed")); + } + let recipe = geometry + .get("binding_recipe") + .cloned() + .ok_or_else(|| Failure::integrity("plan_mismatch"))?; + check_store(probe, &store_b64, digest, &recipe).await?; + if request.source.mime == "image/svg+xml" { + embedded::verify_svg_carrier(source, &output) + .map_err(|_| Failure::integrity("inline_svg_altered"))?; + } + let result = PublishedResult::Embedded { + source_sha256: request.source.sha256.clone(), + output_sha256: sha256_hex(&output), + content_type: request.source.mime.clone(), + output_b64: STANDARD.encode(&output), + headers: identity_headers(finalized)?, + }; + Ok((output, result)) +} + +/// A verified plan, recorded durably by the worker before it finalizes, so +/// a crash after finalize recovers the receipt rather than planning again. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PlannedMedia { + /// The exact prepare request sent. + pub request: PrepareRequest, + /// The signed release the plan was verified with. + pub release: ProbeRelease, + /// The plan's identifier. + pub plan_id: String, + /// The geometry the probe verified against the source, persisted before + /// finalize as a reconstruction input. Never authoritative on its own: + /// every receipt check requires the API's recovered plan to carry + /// exactly this geometry and verifies the recovered one itself. + pub geometry: Value, + /// The digest the verified hash program yields over the source. + pub digest: String, + /// The idempotency key prepare and finalize send. + pub idempotency_key: String, +} + +/// Plans one media item: extract, prepare and the probe's plan check, then +/// the local hash program. No source byte is uploaded, and nothing is +/// signed yet. +/// +/// # Errors +/// +/// A [`Failure`] naming the step that refused. +pub async fn plan_media( + transport: &T, + probe: &LoadedProbe, + capabilities: &Capabilities, + job: &MediaJob<'_>, + source: &S, +) -> Result { + if !capabilities.supports(job.mime, job.carrier) { + return Err(Failure::unsupported(match job.carrier { + Carrier::External => "format_unsupported", + Carrier::Embedded => "unsupported_carrier", + })); + } + if capabilities.policy_digest != job.policy_digest { + return Err(Failure::new(FailureClass::PolicyChanged, "policy_changed")); + } + if source.size() == 0 || source.size() > capabilities.total_size_bytes { + return Err(Failure::unsupported("media_too_large")); + } + let structure = extract(probe, source, job.mime, job.carrier).await?; + let request = prepare_request(job, probe, source.size(), structure); + let prepared = post_json( + transport, + PREPARE_PATH, + &request, + job.idempotency_key, + MAX_PREPARE_BYTES, + ) + .await?; + let (plan_id, geometry) = checked_plan(&prepared, &request)?; + let digest = verified_digest(probe, &request, &geometry, source).await?; + Ok(PlannedMedia { + request, + release: probe.release(), + plan_id, + geometry, + digest, + idempotency_key: job.idempotency_key.to_owned(), + }) +} + +/// Finalizes a verified plan by its digest, then recovers the exact +/// receipt and rebuilds the item from it with +/// [`reconstruct_media`], so the worker's own output is made exactly as the +/// delivery instance will check it. An embedded item needs its whole +/// `source`. Returns embedded output bytes, or `None` for an external item. +/// +/// # Errors +/// +/// A [`Failure`] naming the step that refused. +pub async fn finalize_media( + transport: &T, + probe: &LoadedProbe, + planned: &PlannedMedia, + whole_source: Option<&[u8]>, +) -> Result>, Failure> { + same_release(probe, &planned.request)?; + if planned.request.carrier() == Carrier::Embedded && whole_source.is_none() { + return Err(Failure::unsupported("embedded_requires_bounded_source")); + } + let answer_limit = MAX_FINALIZED_MANIFEST_BYTES + .saturating_mul(4) + .div_ceil(3) + .saturating_add(FINALIZE_OVERHEAD_BYTES); + post_json( + transport, + FINALIZE_PATH, + &finalize_body(planned)?, + &planned.idempotency_key, + answer_limit, + ) + .await?; + let recovered = recover_media(transport, &planned.request, &planned.idempotency_key).await?; + reconstruct_media(probe, planned, &recovered, whole_source).await +} + +/// The exact completed receipt the API recovers for a prepare request. +#[derive(Debug, Clone)] +pub struct RecoveredMedia { + prepare: Value, + finalize: Value, +} + +/// Recovers the API's exact completed receipt for `request` under +/// `idempotency_key`, without preparing, finalizing or reserving anything. +/// +/// # Errors +/// +/// A [`FailureClass::Missing`] failure where the API holds none, and a +/// [`Failure`] for any other refusal or a receipt of another protocol or +/// policy. +pub async fn recover_media( + transport: &T, + request: &PrepareRequest, + idempotency_key: &str, +) -> Result { + let answer = post_json( + transport, + MEDIA_RECOVER_PATH, + request, + idempotency_key, + MAX_FINALIZED_MANIFEST_BYTES + .saturating_mul(4) + .div_ceil(3) + .saturating_add(MAX_PREPARE_BYTES) + .saturating_add(FINALIZE_OVERHEAD_BYTES), + ) + .await?; + if text_field(&answer, "protocol") != Some(PREPARED_PROTOCOL) + || text_field(&answer, "policy_digest") != Some(request.expected_policy_digest.as_str()) + { + return Err(Failure::integrity("receipt_invalid")); + } + let prepare = answer + .get("prepare") + .cloned() + .ok_or_else(|| Failure::integrity("receipt_invalid"))?; + let finalize = answer + .get("finalize") + .cloned() + .ok_or_else(|| Failure::integrity("receipt_invalid"))?; + Ok(RecoveredMedia { prepare, finalize }) +} + +/// Whether `probe` is exactly the release `request` was planned with. +fn same_release(probe: &LoadedProbe, request: &PrepareRequest) -> Result<(), Failure> { + let (sha256, planner_id) = probe.identity(); + if sha256 == request.probe.sha256 && planner_id == request.probe.planner_id { + Ok(()) + } else { + Err(Failure::integrity("probe_release_mismatch")) + } +} + +/// The geometry of the API's recovered plan, after its plan check, where it +/// is exactly the plan `planned` recorded: the same plan identifier in the +/// prepare and finalize answers and the same verified geometry. The +/// recovered geometry is the authoritative one; the recorded copy only has +/// to agree with it. +fn recovered_geometry( + planned: &PlannedMedia, + recovered: &RecoveredMedia, +) -> Result { + let (plan_id, geometry) = checked_plan(&recovered.prepare, &planned.request)?; + if plan_id != planned.plan_id + || text_field(&recovered.finalize, "plan_id") != Some(planned.plan_id.as_str()) + { + return Err(Failure::integrity("receipt_plan_mismatch")); + } + if geometry != planned.geometry { + return Err(Failure::integrity("receipt_geometry_mismatch")); + } + Ok(geometry) +} + +/// The embedded output and result a recovered receipt supports over +/// `source`, the hash program of the recovered plan run here over the +/// actual bytes. +async fn embedded_from_receipt( + probe: &LoadedProbe, + planned: &PlannedMedia, + recovered: &RecoveredMedia, + source: &[u8], +) -> Result<(Vec, PublishedResult), Failure> { + let request = &planned.request; + if sha256_hex(source) != request.source.sha256 + || u64::try_from(source.len()).ok() != Some(request.source.byte_length) + { + return Err(Failure::integrity("source_digest_mismatch")); + } + let geometry = recovered_geometry(planned, recovered)?; + let digest = verified_digest(probe, request, &geometry, source).await?; + if digest != planned.digest { + return Err(Failure::integrity("plan_digest_mismatch")); + } + embedded_output( + probe, + request, + &geometry, + &digest, + &recovered.finalize, + source, + MAX_FINALIZED_MANIFEST_BYTES, + ) + .await +} + +/// Rebuilds a completed item from its recovered receipt with exactly the +/// release it was planned with, without preparing or finalizing again: the +/// worker's path after a crash, a lost publication or an evicted result. +/// An embedded item needs its whole `source`. Plan expiry is never checked: +/// a completed receipt is reconstructed whatever its plan's lifetime was. +/// Returns embedded output bytes, or `None` for an external item. +/// +/// # Errors +/// +/// A [`Failure`] for a receipt that does not verify against the recorded +/// plan and source. +pub async fn reconstruct_media( + probe: &LoadedProbe, + planned: &PlannedMedia, + recovered: &RecoveredMedia, + whole_source: Option<&[u8]>, +) -> Result>, Failure> { + let request = &planned.request; + same_release(probe, request)?; + match (request.carrier(), whole_source) { + (Carrier::External, _) => { + recovered_geometry(planned, recovered)?; + external_result( + probe, + request, + &recovered.finalize, + MAX_FINALIZED_MANIFEST_BYTES, + ) + .await?; + Ok(None) + } + (Carrier::Embedded, Some(source)) => { + let (output, _) = embedded_from_receipt(probe, planned, recovered, source).await?; + Ok(Some(output)) + } + (Carrier::Embedded, None) => Err(Failure::unsupported("embedded_requires_bounded_source")), + } +} + +/// Refuses `source` where it carries provenance of its own: an embedded +/// manifest, or an XMP reference to one. An external manifest delivered +/// over either would never be read, so the source keeps it, whatever the +/// worker's disclosure showed. Runs exactly the release `request` was +/// planned with, under its probe budgets, over the caller's actual bytes: +/// a bounded item's hash-checked bytes or a large one's pinned version. +/// +/// # Errors +/// +/// A [`FailureClass::ExistingProvenance`] failure for a source with +/// provenance, and a [`Failure`] where `probe` is not the request's release +/// or cannot inspect the source. +pub async fn inspect_external_source( + probe: &LoadedProbe, + request: &PrepareRequest, + source: &S, +) -> Result<(), Failure> { + same_release(probe, request)?; + inspect_provenance(probe, source, &request.source.mime).await?; + Ok(()) +} + +/// Checks a recovered external receipt against `planned`, whose source the +/// caller verified independently and inspected with +/// [`inspect_external_source`]: the bytes of a bounded item hashed by the +/// caller, a large one by its own pinned whole read. Needs no capabilities, +/// so a completed receipt is checked while new preparation is closed. +/// +/// # Errors +/// +/// A [`Failure`] for a receipt that does not bind the request's exact +/// source, policy, plan and manifest. +pub async fn verify_external_receipt( + probe: &LoadedProbe, + planned: &PlannedMedia, + recovered: &RecoveredMedia, +) -> Result { + let request = &planned.request; + if request.carrier() != Carrier::External { + return Err(Failure::integrity("carrier_mismatch")); + } + if planned.digest != request.source.sha256 { + return Err(Failure::integrity("plan_digest_mismatch")); + } + same_release(probe, request)?; + recovered_geometry(planned, recovered)?; + external_result( + probe, + request, + &recovered.finalize, + MAX_FINALIZED_MANIFEST_BYTES, + ) + .await +} + +/// Checks a recovered embedded receipt against the actual `source` bytes +/// and the worker's `output`. The worker's disclosure must be the source's +/// own bytes, and the source must carry no provenance of its own, however +/// little of it the disclosure shows. The probe's plan check and hash +/// program over the source must yield the digest the receipt signed, and +/// the output the delivery instance builds itself from the receipt must be +/// exactly the worker's and walk back to that digest. A self-consistent +/// output with changed pixels, markup or structure is refused. Needs no +/// capabilities. +/// +/// # Errors +/// +/// A [`Failure`] for any mismatch. +pub async fn verify_embedded_receipt( + probe: &LoadedProbe, + planned: &PlannedMedia, + recovered: &RecoveredMedia, + source: &[u8], + output: &[u8], +) -> Result { + if planned.request.carrier() != Carrier::Embedded { + return Err(Failure::integrity("carrier_mismatch")); + } + same_release(probe, &planned.request)?; + // The disclosure seeds the probe's plan check, and an embedded one never + // shows the XMP: neither is taken on the worker's word. + check_structure(&planned.request.structure, source)?; + inspect_provenance(probe, source, &planned.request.source.mime).await?; + let (built, result) = embedded_from_receipt(probe, planned, recovered, source).await?; + if built != output { + return Err(Failure::integrity("output_substituted")); + } + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::executor::block_on; + + #[test] + fn prepared_text_requires_the_exact_request_cache_scope() { + let request = json!({ + "text": "Harbor council votes", + "page_url": "https://www.example.com/news/a", + "document_title": "Harbor", + "boundary_selector": "article", + "expected_policy_digest": "a".repeat(64), + "cache_scope": "exact_request", + }); + let parsed: TextSignRequest = + serde_json::from_value(request.clone()).expect("should admit the exact scope"); + assert_eq!( + serde_json::to_value(parsed).expect("should preserve the frozen request"), + request + ); + let mut missing = request.clone(); + missing + .as_object_mut() + .expect("should be an object") + .remove("cache_scope"); + assert!( + serde_json::from_value::(missing).is_err(), + "an old request must not silently inherit the current cache contract" + ); + for scope in [ + Value::Null, + json!(""), + json!("legacy"), + json!("EXACT_REQUEST"), + ] { + let mut unsupported = request.clone(); + unsupported["cache_scope"] = scope; + assert!( + serde_json::from_value::(unsupported).is_err(), + "only the exact_request cache scope is admitted" + ); + } + } + + #[test] + fn synchronous_text_request_keeps_its_existing_wire_contract() { + let request = api::SignRequest { + text: "Harbor council votes", + page_url: "https://www.example.com/news/a", + document_title: Some("Harbor"), + boundary_selector: "article", + expected_policy_digest: "policy", + }; + assert_eq!( + serde_json::to_value(request).expect("should serialize the synchronous request"), + json!({ + "text": "Harbor council votes", + "page_url": "https://www.example.com/news/a", + "document_title": "Harbor", + "boundary_selector": "article", + "expected_policy_digest": "policy", + }), + "synchronous signing must not opt into prepared cache isolation" + ); + } + + #[test] + fn compose_applies_edits_in_order_and_refuses_overlap_and_wrong_sizes() { + let source = b"abcdefgh"; + let geometry = json!({ + "edits": [ + {"op": "patch", "offset": 1, "b64": STANDARD.encode(b"XY")}, + {"op": "insert_carrier", "offset": 4, "length": 3}, + {"op": "delete", "offset": 6, "length": 2}, + ], + "expected_final_size": 9, + }); + let output = compose(source, &geometry, b"CCC").expect("should compose"); + assert_eq!(output, b"aXYdCCCef", "should apply every edit in order"); + + let wrong_carrier = compose(source, &geometry, b"CC").expect_err("should refuse"); + assert_eq!(wrong_carrier.code, "carrier_mismatch"); + + let overlapping = json!({ + "edits": [ + {"op": "delete", "offset": 4, "length": 2}, + {"op": "patch", "offset": 3, "b64": STANDARD.encode(b"Z")}, + ], + "expected_final_size": 6, + }); + assert_eq!( + compose(source, &overlapping, b"") + .expect_err("should refuse") + .code, + "plan_mismatch" + ); + + let wrong_size = json!({"edits": [], "expected_final_size": 7}); + assert_eq!( + compose(source, &wrong_size, b"") + .expect_err("should refuse") + .code, + "output_verification_failed" + ); + } + + #[test] + fn hash_program_hashes_ranges_and_literals_and_refuses_out_of_bounds() { + let source = b"hello world"; + let items = json!([ + {"range": [0, 5]}, + {"literal": STANDARD.encode(b"!")}, + {"range": [5, 6]}, + ]); + let digest = futures::executor::block_on(run_hash_program(&items, source.as_slice())) + .expect("should hash"); + assert_eq!( + digest, + sha256_hex(b"hello! world"), + "should hash in program order" + ); + let beyond = json!([{"range": [6, 6]}]); + assert_eq!( + futures::executor::block_on(run_hash_program(&beyond, source.as_slice())) + .expect_err("should refuse") + .code, + "hash_program_out_of_bounds" + ); + } + + #[test] + fn whole_source_program_is_only_one_unexcluded_range() { + assert!(covers_whole_source(&json!([{"range": [0, 10]}]), 10)); + assert!(!covers_whole_source(&json!([{"range": [0, 9]}]), 10)); + assert!(!covers_whole_source( + &json!([{"range": [0, 5]}, {"range": [5, 5]}]), + 10 + )); + assert!(!covers_whole_source( + &json!([{"range": [0, 10], "extra": 1}]), + 10 + )); + } + + #[test] + fn capabilities_decide_carrier_support_from_engine_advertisement() { + let body = json!({ + "protocol": PREPARED_PROTOCOL, + "enabled": true, + "policy_digest": "a".repeat(64), + "formats": ["image/jpeg"], + "contract_versions": [1, 2], + "embedded_formats": ["image/png"], + "maximums": {"total_size_bytes": 10, "manifest_decoded_bytes": 10}, + "probe": [], + }); + let parsed = Capabilities::parse(body.to_string().as_bytes()).expect("should parse"); + assert!(parsed.supports("image/jpeg", Carrier::External)); + assert!(!parsed.supports("image/jpeg", Carrier::Embedded)); + assert!(parsed.supports("image/png", Carrier::Embedded)); + + let mut legacy = body.clone(); + legacy + .as_object_mut() + .expect("object") + .remove("contract_versions"); + let legacy = Capabilities::parse(legacy.to_string().as_bytes()).expect("should parse"); + assert!( + !legacy.supports("image/png", Carrier::Embedded), + "an engine that never advertised version 2 supports no new carrier" + ); + } + + // --- the publication boundary over the real signed probe --------------- + + const TIFF_MIME: &str = "image/tiff"; + const PHOTOMETRIC: u16 = 262; + const TIFF_CARRIER_LENGTH: u64 = 4097; + const POLICY_DIGEST: &str = "1111111111111111111111111111111111111111111111111111111111111111"; + const PLAN_ID: &str = "00000000-0000-4000-8000-000000000018"; + const IDEMPOTENCY_KEY: &str = "tiff-publication-fixture"; + + /// The production-signed release 18 probe every retained receipt + /// fixture was planned with. + fn signed_probe() -> LoadedProbe { + let release: ProbeRelease = + serde_json::from_slice(include_bytes!("../fixtures/prepared/descriptor.json")) + .expect("should parse the production-signed descriptor"); + let module = block_on(ProbeModule::load( + &ReleaseAuthority::production(), + &release.descriptor(), + include_bytes!("../fixtures/prepared/probe.wasm"), + ProbeLimits::default(), + )) + .expect("should load the production-signed probe"); + LoadedProbe(module) + } + + fn le_u16(bytes: &[u8], at: usize) -> u16 { + u16::from_le_bytes([bytes[at], bytes[at + 1]]) + } + + fn le_u32(bytes: &[u8], at: usize) -> u32 { + u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]) + } + + /// One classic little-endian IFD entry. + fn tiff_entry(tag: u16, kind: u16, count: u32, value: u32) -> [u8; 12] { + let mut entry = [0_u8; 12]; + entry[..2].copy_from_slice(&tag.to_le_bytes()); + entry[2..4].copy_from_slice(&kind.to_le_bytes()); + entry[4..8].copy_from_slice(&count.to_le_bytes()); + entry[8..].copy_from_slice(&value.to_le_bytes()); + entry + } + + /// A manifest-free, single-IFD, uncompressed 4x2 grayscale TIFF whose + /// samples read as `BlackIsZero`, with `xmp` as its tag 700 packet. + fn grayscale_tiff(xmp: Option<&[u8]>) -> Vec { + const SAMPLES: [u8; 8] = [0, 32, 64, 96, 128, 160, 192, 255]; + let count: u16 = if xmp.is_some() { 10 } else { 9 }; + let pixels = 8 + 2 + 12 * u32::from(count) + 4; + let mut entries = vec![ + tiff_entry(256, 3, 1, 4), + tiff_entry(257, 3, 1, 2), + tiff_entry(258, 3, 1, 8), + tiff_entry(259, 3, 1, 1), + tiff_entry(PHOTOMETRIC, 3, 1, 1), + tiff_entry(273, 4, 1, pixels), + tiff_entry(277, 3, 1, 1), + tiff_entry(278, 3, 1, 2), + tiff_entry(279, 4, 1, 8), + ]; + if let Some(xmp) = xmp { + let length = u32::try_from(xmp.len()).expect("should fit"); + entries.push(tiff_entry(700, 1, length, pixels + 8)); + } + let mut tiff = b"II".to_vec(); + tiff.extend_from_slice(&42_u16.to_le_bytes()); + tiff.extend_from_slice(&8_u32.to_le_bytes()); + tiff.extend_from_slice(&count.to_le_bytes()); + for entry in &entries { + tiff.extend_from_slice(entry); + } + tiff.extend_from_slice(&0_u32.to_le_bytes()); + tiff.extend_from_slice(&SAMPLES); + if let Some(xmp) = xmp { + tiff.extend_from_slice(xmp); + } + tiff + } + + /// The entries of the IFD the header of `tiff` points at. + fn active_entries(tiff: &[u8]) -> Vec<[u8; 12]> { + let ifd = usize::try_from(le_u32(tiff, 4)).expect("should fit"); + (0..usize::from(le_u16(tiff, ifd))) + .map(|index| { + let start = ifd + 2 + 12 * index; + tiff[start..start + 12] + .try_into() + .expect("should be one entry") + }) + .collect() + } + + /// The active IFD's `PhotometricInterpretation`. + fn photometric(tiff: &[u8]) -> Option { + active_entries(tiff) + .iter() + .find(|entry| le_u16(entry.as_slice(), 0) == PHOTOMETRIC) + .map(|entry| le_u16(entry.as_slice(), 8)) + } + + /// The canonical C2PA 2.4 `main_ifd_when_single` plan for a classic + /// little-endian TIFF of `source_length` bytes whose one IFD holds + /// `entries`, laid out as the engine's TIFF planner lays it: the header + /// repointed at a rebuilt IFD after the carrier, holding those entries + /// and the C2PA entry. + fn tiff_geometry(source_length: u64, entries: &[[u8; 12]]) -> Value { + let carrier = TIFF_CARRIER_LENGTH; + let leading = source_length % 2; + let manifest_length = carrier - leading; + let trailing = manifest_length % 2; + let rebuilt_at = source_length + carrier + trailing; + let mut rebuilt = entries.to_vec(); + rebuilt.push(tiff_entry( + 0xCD41, + 7, + u32::try_from(manifest_length).expect("should fit"), + u32::try_from(source_length + leading).expect("should fit"), + )); + rebuilt.sort_by_key(|entry| le_u16(entry.as_slice(), 0)); + let mut inserted = vec![0_u8; usize::try_from(trailing).expect("should fit")]; + inserted.extend_from_slice( + &u16::try_from(rebuilt.len()) + .expect("should fit") + .to_le_bytes(), + ); + for entry in &rebuilt { + inserted.extend_from_slice(entry); + } + inserted.extend_from_slice(&0_u32.to_le_bytes()); + let pointer = u32::try_from(rebuilt_at).expect("should fit").to_le_bytes(); + let inserted_length = u64::try_from(inserted.len()).expect("should fit"); + json!({ + "disposition": "embedded", + "edits": [ + {"op": "patch", "offset": 4, "b64": STANDARD.encode(pointer)}, + {"op": "insert_carrier", "offset": source_length, "length": carrier}, + {"op": "insert_bytes", "offset": source_length, "b64": STANDARD.encode(&inserted)}, + ], + "carrier_span": [0, carrier], + "carrier_output_span": [source_length, carrier], + "expected_final_size": source_length + carrier + inserted_length, + "binding_recipe": { + "type": "data_hash", + "algorithm": "sha256", + "exclusions": [{"offset": source_length, "length": carrier}], + }, + "carrier_recipe": { + "type": "tiff", + "big": false, + "little_endian": true, + "entries": entries.iter().map(|entry| entry.to_vec()).collect::>(), + "next_ifd": 0, + "original_size": source_length, + }, + "planner_options": {"tiff_placement": "main_ifd_when_single"}, + }) + } + + fn length_of(source: &[u8]) -> u64 { + u64::try_from(source.len()).expect("should fit") + } + + fn tiff_request( + probe: &LoadedProbe, + source: &[u8], + structure: Vec, + ) -> PrepareRequest { + let source_sha256 = sha256_hex(source); + let reference = + format!("https://publisher.example.com/_encypher/preparation/source/{source_sha256}"); + let job = MediaJob { + page_url: "https://publisher.example.com/article", + policy_digest: POLICY_DIGEST, + carrier: Carrier::Embedded, + mime: TIFF_MIME, + source_sha256: &source_sha256, + reference: &reference, + origin_host: "publisher.example.com", + reference_kind: "publisher_snapshot", + idempotency_key: IDEMPOTENCY_KEY, + }; + prepare_request(&job, probe, length_of(source), structure) + } + + /// The plan record for `request` and a recovered receipt carrying + /// `geometry`. The finalize answer names the plan but holds no signed + /// manifest: no genuine receipt for a TIFF is among the fixtures, so a + /// check that gets this far accepted the structure, the geometry and the + /// digest over the source, and refuses only for want of a signature. + fn tiff_receipt( + probe: &LoadedProbe, + request: PrepareRequest, + geometry: &Value, + digest: String, + ) -> (PlannedMedia, RecoveredMedia) { + let recovered = RecoveredMedia { + prepare: json!({ + "plan_id": PLAN_ID, + "policy_digest": POLICY_DIGEST, + "geometry": geometry, + }), + finalize: json!({"plan_id": PLAN_ID, "policy_digest": POLICY_DIGEST}), + }; + let planned = PlannedMedia { + request, + release: probe.release(), + plan_id: PLAN_ID.to_owned(), + geometry: geometry.clone(), + digest, + idempotency_key: IDEMPOTENCY_KEY.to_owned(), + }; + (planned, recovered) + } + + /// `structure` with the `PhotometricInterpretation` value rewritten to + /// `value`, every other disclosed byte as it was. + fn with_photometric(structure: &[StructureChunk], value: u16) -> Vec { + // The fifth entry of the IFD at offset 8; its value field starts + // eight bytes into the entry. + let field = 8 + 2 + 12 * 4 + 8; + let mut forged = structure.to_vec(); + let chunk = forged + .iter_mut() + .find(|chunk| { + let length = STANDARD + .decode(&chunk.b64) + .map_or(0, |bytes| length_of(&bytes)); + chunk.offset <= field && field + 2 <= chunk.offset + length + }) + .expect("the disclosure should hold the photometric entry"); + let mut bytes = STANDARD.decode(&chunk.b64).expect("should be base64"); + let at = usize::try_from(field - chunk.offset).expect("should fit"); + bytes[at..at + 2].copy_from_slice(&value.to_le_bytes()); + chunk.b64 = STANDARD.encode(bytes); + forged + } + + fn zero_carrier() -> Vec { + vec![0_u8; usize::try_from(TIFF_CARRIER_LENGTH).expect("should fit")] + } + + /// A worker that discloses a false TIFF structure for the true source + /// gets a plan the signed probe verifies over the true bytes, because + /// the disclosure seeds the probe's parse: the output would carry a false + /// `PhotometricInterpretation` under the true source digest. Publication + /// refuses it, and the honest structure still passes every source check. + #[test] + fn publication_refuses_a_disclosure_the_source_does_not_hold() { + let probe = signed_probe(); + let source = grayscale_tiff(None); + let honest = block_on(extract( + &probe, + source.as_slice(), + TIFF_MIME, + Carrier::Embedded, + )) + .expect("the signed probe should extract a manifest-free TIFF"); + let forged = with_photometric(&honest, 0); + assert_ne!(forged, honest, "the forgery should change the disclosure"); + + let honest_geometry = tiff_geometry(length_of(&source), &active_entries(&source)); + let mut forged_entries = active_entries(&source); + forged_entries[4][8..10].copy_from_slice(&0_u16.to_le_bytes()); + let forged_geometry = tiff_geometry(length_of(&source), &forged_entries); + + // The geometry is the engine's own: the probe verifies the honest + // plan over the true source and refuses the forged one there. + let honest_request = tiff_request(&probe, &source, honest); + let honest_digest = block_on(verified_digest( + &probe, + &honest_request, + &honest_geometry, + source.as_slice(), + )) + .expect("the signed probe should verify the canonical plan"); + let refused = block_on(verified_digest( + &probe, + &honest_request, + &forged_geometry, + source.as_slice(), + )) + .expect_err("an honest disclosure should not admit the forged plan"); + assert_eq!( + refused.class, + FailureClass::Integrity, + "the forged plan should be a plan mismatch over honest structure" + ); + + // Seeded with the false structure, the same probe verifies the + // forged plan over the same true bytes. + let forged_request = tiff_request(&probe, &source, forged); + assert_eq!( + forged_request.source, honest_request.source, + "the forged request should name the true source, digest and length" + ); + let forged_digest = block_on(verified_digest( + &probe, + &forged_request, + &forged_geometry, + source.as_slice(), + )) + .expect("the signed probe should take the disclosure as the source's structure"); + assert_ne!( + forged_digest, honest_digest, + "the forged plan should bind other bytes" + ); + + // What that plan delivers: the true samples under a WhiteIsZero + // active IFD. + let honest_output = + compose(&source, &honest_geometry, &zero_carrier()).expect("should compose"); + let forged_output = + compose(&source, &forged_geometry, &zero_carrier()).expect("should compose"); + assert_eq!(photometric(&source), Some(1), "the source is BlackIsZero"); + assert_eq!( + photometric(&honest_output), + Some(1), + "the honest output should keep BlackIsZero" + ); + assert_eq!( + photometric(&forged_output), + Some(0), + "the forged output should read the same samples as WhiteIsZero" + ); + assert_eq!( + &forged_output[8..source.len()], + &source[8..], + "every source byte after the header pointer should be unchanged" + ); + + // Publication refuses the forged structure before it reads the + // receipt; the honest structure reaches the receipt. + let (planned, recovered) = + tiff_receipt(&probe, forged_request, &forged_geometry, forged_digest); + let failure = block_on(verify_embedded_receipt( + &probe, + &planned, + &recovered, + &source, + &forged_output, + )) + .expect_err("publication should refuse a disclosure the source does not hold"); + assert_eq!( + failure, + Failure::integrity("structure_mismatch"), + "the forged disclosure should be refused as not the source's own" + ); + + let (planned, recovered) = + tiff_receipt(&probe, honest_request, &honest_geometry, honest_digest); + let failure = block_on(verify_embedded_receipt( + &probe, + &planned, + &recovered, + &source, + &honest_output, + )) + .expect_err("the fixture receipt holds no signed manifest"); + assert_eq!( + failure.code, "finalize_answer_invalid", + "the honest structure should pass every source check and stop only at the unsigned receipt" + ); + } + + /// A worker that skips the provenance inspection and discloses only the + /// embedded structure of a TIFF whose XMP names a remote manifest gets a + /// plan the probe verifies over the true bytes: the packet lies outside + /// the structure. Publication inspects the source itself and keeps its + /// provenance. + #[test] + fn publication_keeps_provenance_the_disclosure_leaves_out() { + let probe = signed_probe(); + let xmp = br#""#; + let source = grayscale_tiff(Some(xmp)); + let kept = block_on(extract( + &probe, + source.as_slice(), + TIFF_MIME, + Carrier::Embedded, + )) + .expect_err("the worker's extract should keep the source's provenance"); + assert_eq!( + kept, + Failure::new( + FailureClass::ExistingProvenance, + "source_has_provenance_reference" + ), + "the signed probe should find the XMP reference" + ); + + // The embedded extract alone, as a worker skipping the inspection + // runs it. + let embedded = block_on(probe.0.call( + "extract", + &json!({"mime": TIFF_MIME, "total_size": length_of(&source)}), + &Probed(source.as_slice()), + )) + .map_err(|error| probe_failure("extract", error)) + .expect("the embedded extract should not read the XMP"); + let structure = embedded["chunks"] + .as_array() + .expect("should disclose chunks") + .iter() + .map(|chunk| StructureChunk { + offset: chunk["offset"].as_u64().expect("should have an offset"), + b64: chunk["data"].as_str().expect("should have data").to_owned(), + }) + .collect(); + let geometry = tiff_geometry(length_of(&source), &active_entries(&source)); + let request = tiff_request(&probe, &source, structure); + let digest = block_on(verified_digest( + &probe, + &request, + &geometry, + source.as_slice(), + )) + .expect("the signed probe should verify the plan over the true source"); + let output = compose(&source, &geometry, &zero_carrier()).expect("should compose"); + + let (planned, recovered) = tiff_receipt(&probe, request, &geometry, digest); + let failure = block_on(verify_embedded_receipt( + &probe, &planned, &recovered, &source, &output, + )) + .expect_err("publication should keep the source's provenance"); + assert_eq!( + failure, + Failure::new( + FailureClass::ExistingProvenance, + "source_has_provenance_reference" + ), + "publication should inspect the source it publishes over" + ); + } +} diff --git a/crates/provenance/encypher/src/preparation/protocol.rs b/crates/provenance/encypher/src/preparation/protocol.rs new file mode 100644 index 0000000000..a4b6693e13 --- /dev/null +++ b/crates/provenance/encypher/src/preparation/protocol.rs @@ -0,0 +1,1075 @@ +//! The `encypher-preparation/1` protocol between a publisher's delivery +//! instance and its native preparation worker. +//! +//! Everything both sides must agree on byte for byte lives here: the +//! preparation identities, the published records readers look up, the +//! retirement markers, the reader hint, the snapshot capability and the +//! snapshot facts. The reader and the worker call these functions, never a +//! copy of them, so an identity the reader computes from the bytes it +//! delivers is the identity the worker signs under. +//! +//! An identity is a versioned, domain-separated SHA-256 over +//! length-delimited parts. A part's length is written before it, so no two +//! different lists of parts hash alike, and the domain names the kind of +//! item and its identity contract version, so no identity of one kind matches +//! another's. Identity versions can change without changing the wire protocol. + +use std::collections::BTreeMap; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use hmac::{Hmac, KeyInit as _, Mac as _}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest as _, Sha256}; +use subtle::ConstantTimeEq as _; + +use crate::api::EmbeddingPlan; +use crate::media_api::{hex_lower, sha256_hex}; + +/// The protocol every message between the delivery instance and the worker +/// carries. +pub const PREPARATION_PROTOCOL: &str = "encypher-preparation/1"; + +/// The version of the text projection an identity binds. A corrected +/// projection takes a new version, so it never reuses an old plan. +pub const TEXT_PROJECTION_VERSION: &str = "visible-text/1"; + +/// The request header the worker and the delivery instance authenticate +/// control requests with. Its value is the scope's secret, compared in +/// constant time, never logged. +pub const TOKEN_HEADER: &str = "x-encypher-preparation-token"; + +/// The request header carrying a snapshot capability on the delivery +/// instance's own loopback read. It never reaches an origin. +pub const CAPABILITY_HEADER: &str = "x-encypher-preparation-capability"; + +/// The response header reporting what the origin's own headers said about a +/// snapshot, authenticated for one capability's nonce. +pub const FACTS_HEADER: &str = "x-encypher-preparation-facts"; + +/// The delivery instance's snapshot route. +pub const SNAPSHOT_PATH: &str = "/_encypher/preparation/snapshot"; +/// The delivery instance's result publication route, below which an +/// identity follows. +pub const RESULTS_PREFIX: &str = "/_encypher/preparation/results/"; +/// The delivery instance's retirement marker route, below which an identity +/// follows. +pub const MARKERS_PREFIX: &str = "/_encypher/preparation/markers/"; +/// The publisher route serving explicitly public source snapshots, below +/// which a raw-source SHA-256 follows. +pub const PUBLIC_SOURCE_PREFIX: &str = "/_encypher/preparation/source/"; + +/// The worker's reader-signal route. +pub const SIGNALS_PATH: &str = "/v1/preparation/signals"; +/// The worker's operator warming route. +pub const WARM_PATH: &str = "/v1/preparation/warm"; +/// The worker's operator retirement route. +pub const RETIRE_PATH: &str = "/v1/preparation/retire"; +/// The worker's operator status route. +pub const STATUS_PATH: &str = "/v1/preparation/status"; +/// The worker's readiness route. +pub const READY_PATH: &str = "/v1/preparation/ready"; +/// The worker's public source route, which only the delivery instance +/// calls, below which a raw-source SHA-256 follows. +pub const WORKER_SOURCE_PREFIX: &str = "/v1/preparation/sources/"; + +/// The largest reader hint, in bytes. +pub const MAX_SIGNAL_BYTES: usize = 8 * 1024; +/// The largest target path a hint or warm names. +pub const MAX_TARGET_PATH_BYTES: usize = 1024; +/// The most variant parameters a schema names. +pub const MAX_VARIANT_PARAMS: usize = 8; +/// The most values one variant parameter admits. +pub const MAX_VARIANT_VALUES: usize = 32; + +/// How long a snapshot capability stays valid, in seconds. +pub const CAPABILITY_LIFETIME_SECONDS: u64 = 120; + +type HmacSha256 = Hmac; + +/// SHA-256, lower case hex, over `domain` and each part, every part's +/// length written before it. +#[must_use] +pub fn domain_digest(domain: &str, parts: &[&[u8]]) -> String { + let mut digest = Sha256::new(); + digest.update( + u64::try_from(domain.len()) + .unwrap_or(u64::MAX) + .to_be_bytes(), + ); + digest.update(domain.as_bytes()); + for part in parts { + digest.update(u64::try_from(part.len()).unwrap_or(u64::MAX).to_be_bytes()); + digest.update(part); + } + hex_lower(&digest.finalize()) +} + +/// Whether `value` is a lower case SHA-256 hex digest, the shape of every +/// identity. +#[must_use] +pub fn is_digest(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +/// The credential context every identity binds: the API origin, the claimed +/// domain and the worker credential, hashed, so two credentials or two +/// publishers never share a result. Never a secret. +#[must_use] +pub fn credential_context(api_base_url: &str, domain: &str, worker_token: &str) -> String { + let hash = sha256_hex(format!("{api_base_url}\n{domain}\n{}", worker_token.trim()).as_bytes()); + hash.get(..16).unwrap_or(hash.as_str()).to_owned() +} + +/// What an article's preparation identity binds: every field of the sign +/// request that affects its signed assertions, the projection version and +/// the admitted variant. Its text-only hash domain also versions the API +/// cache contract: version 2 requires exact-request isolation, so records +/// made through the old coarse cache never satisfy a current lookup. +#[derive(Debug, Clone, Copy)] +pub struct TextIdentity<'a> { + /// See [`credential_context`]. + pub credential_context: &'a str, + /// The policy the article is signed under. + pub policy_digest: &'a str, + /// The query-free page address the article is signed for. + pub page_url: &'a str, + /// The admitted variant, see [`VariantSchema::admit`]. + pub variant: &'a str, + /// The exact visible text sent for signing. + pub text: &'a str, + /// The page title sent, if any. + pub document_title: Option<&'a str>, + /// The article boundary selector sent. + pub boundary_selector: &'a str, +} + +impl TextIdentity<'_> { + /// The identity. + #[must_use] + pub fn digest(&self) -> String { + let text = sha256_hex(self.text.as_bytes()); + let (title_present, title) = match self.document_title { + Some(title) => (&b"1"[..], title.as_bytes()), + None => (&b"0"[..], &b""[..]), + }; + domain_digest( + "encypher-preparation/2:text", + &[ + self.credential_context.as_bytes(), + self.policy_digest.as_bytes(), + self.page_url.as_bytes(), + self.variant.as_bytes(), + TEXT_PROJECTION_VERSION.as_bytes(), + text.as_bytes(), + title_present, + title, + self.boundary_selector.as_bytes(), + ], + ) + } +} + +/// How a media item's manifest is delivered. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Carrier { + /// Unchanged bytes and a remote manifest named by a `Link`. + External, + /// The manifest inside the delivered bytes. + Embedded, +} + +impl Carrier { + /// The name the protocol writes. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::External => "external", + Self::Embedded => "embedded", + } + } +} + +/// What a bounded media item's preparation identity binds. +#[derive(Debug, Clone, Copy)] +pub struct MediaIdentity<'a> { + /// See [`credential_context`]. + pub credential_context: &'a str, + /// The policy the item is signed under. + pub policy_digest: &'a str, + /// The reader-facing address the item is delivered on or in. + pub page_url: &'a str, + /// The admitted variant of that address. + pub variant: &'a str, + /// How the manifest is delivered. + pub carrier: Carrier, + /// The canonical media type of the source. + pub mime: &'a str, + /// SHA-256 of the exact source bytes, lower case hex. + pub source_sha256: &'a str, +} + +impl MediaIdentity<'_> { + /// The identity. + #[must_use] + pub fn digest(&self) -> String { + domain_digest( + "encypher-preparation/1:media", + &[ + self.credential_context.as_bytes(), + self.policy_digest.as_bytes(), + self.page_url.as_bytes(), + self.variant.as_bytes(), + self.carrier.as_str().as_bytes(), + self.mime.as_bytes(), + self.source_sha256.as_bytes(), + ], + ) + } +} + +/// The source-version locator of a large item: the configured origin and +/// the strong validators it answered with. An entity tag is not +/// cryptographic evidence; this relies on a publisher-controlled origin +/// honoring strong validator semantics, and the published record carries +/// the whole-source digest the worker verified. +#[derive(Debug, Clone, Copy)] +pub struct LargeLocator<'a> { + /// See [`credential_context`]. + pub credential_context: &'a str, + /// The policy the item is signed under. + pub policy_digest: &'a str, + /// The query-free reader address. + pub reader_url: &'a str, + /// The admitted variant of that address. + pub variant: &'a str, + /// The lower case host the source is fetched from. + pub origin_host: &'a str, + /// SHA-256 of core's configured-origin identity, not just its hostname. + pub origin_identity_sha256: &'a str, + /// The canonical media type. + pub mime: &'a str, + /// The single strong entity tag, quoted. + pub etag: &'a str, + /// The total length. + pub length: u64, +} + +impl LargeLocator<'_> { + /// The identity. + #[must_use] + pub fn digest(&self) -> String { + let length = self.length.to_string(); + domain_digest( + "encypher-preparation/1:large", + &[ + self.credential_context.as_bytes(), + self.policy_digest.as_bytes(), + self.reader_url.as_bytes(), + self.variant.as_bytes(), + self.origin_host.as_bytes(), + self.origin_identity_sha256.as_bytes(), + self.mime.as_bytes(), + self.etag.as_bytes(), + length.as_bytes(), + ], + ) + } +} + +/// The KV key a published result is stored under. +#[must_use] +pub fn result_key(identity: &str) -> String { + format!("encypher:prep:v1:result:{identity}") +} + +/// The KV key a retirement marker is stored under, apart from the result so +/// a late result can never overwrite it. +#[must_use] +pub fn marker_key(identity: &str) -> String { + format!("encypher:prep:v1:retired:{identity}") +} + +/// A result the delivery instance verified through the authenticated API +/// and stored under its immutable identity. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PublishedRecord { + /// [`PREPARATION_PROTOCOL`]. + pub protocol: String, + /// The identity the record is stored under. + pub identity: String, + /// The retirement epoch the result was prepared under. + pub retirement_epoch: u64, + /// What a reader applies. + pub result: PublishedResult, +} + +/// What a reader applies for one identity. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum PublishedResult { + /// An article's marker plan. + Text { + /// The verified plan. + plan: EmbeddingPlan, + /// The manifest URL written in the head, if any. + manifest_url: Option, + /// The scalars of the text the plan was made for. + scalars: usize, + }, + /// Unchanged bounded media with a remote manifest. + External { + /// The exact source the manifest binds. + source_sha256: String, + /// Headers every answer carries: the manifest `Link` and ids. + headers: Vec<(String, String)>, + }, + /// Bounded media with its manifest embedded. + Embedded { + /// The exact source the output was made from. + source_sha256: String, + /// SHA-256 of the output bytes. + output_sha256: String, + /// The output's media type. + content_type: String, + /// The output bytes, base64. + output_b64: String, + /// Headers every answer carries: the manifest ids. + headers: Vec<(String, String)>, + }, + /// An unchanged large source with a remote manifest. + Large { + /// SHA-256 of every byte of the version. + sha256: String, + /// The version's length. + length: u64, + /// Headers every answer carries. + headers: Vec<(String, String)>, + }, +} + +/// The marker that retires an identity on the delivery instance. A reader +/// refuses the identity's result while it is present, and a result +/// publication never removes it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RetirementMarker { + /// The retirement epoch that retired the identity. + pub epoch: u64, +} + +/// Which kind of item a hint, job or result is about. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ItemKind { + /// An article's text. + Text, + /// A bounded media item delivered on its own address. + Media, + /// A media item embedded in a page or stylesheet. + Embedded, + /// A large media item, by its validator locator. + Large, +} + +/// One item a reader observed and found no published result for. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ObservedItem { + /// What kind of item. + pub kind: ItemKind, + /// Its identity as the reader computed it from what it delivered. + pub identity: String, +} + +/// A configured target: a publisher path and an admitted variant. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Target { + /// The publisher's path, starting with `/`, without a query. + pub path: String, + /// The admitted variant, see [`VariantSchema::admit`], empty for none. + #[serde(default)] + pub variant: String, +} + +impl Target { + /// Whether the target is well formed: an absolute path within its + /// bound, without a query, fragment, `..` segment or control byte. + #[must_use] + pub fn is_well_formed(&self) -> bool { + self.path.starts_with('/') + && !self.path.starts_with("//") + && self.path.len() <= MAX_TARGET_PATH_BYTES + && self + .path + .bytes() + .all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'?' | b'#' | b'\\')) + && !self.path.split('/').any(|segment| segment == "..") + && self.variant.len() <= MAX_TARGET_PATH_BYTES + } + + /// The path and query the delivery instance reads for this target. + #[must_use] + pub fn path_and_query(&self) -> String { + if self.variant.is_empty() { + self.path.clone() + } else { + format!("{}?{}", self.path, self.variant) + } + } +} + +/// A reader's hint that the items it delivered are not prepared. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SignalRequest { + /// [`PREPARATION_PROTOCOL`]. + pub protocol: String, + /// The deployment the hint is for. + pub deployment_id: String, + /// The target the reader was served. + pub target: Target, + /// The items it found unprepared, cache misses first. + pub items: Vec, + /// How many more unprepared items did not fit the hint. + #[serde(default)] + pub unobserved: u32, +} + +impl SignalRequest { + /// The hint's JSON, holding as many of `items` as fit in + /// [`MAX_SIGNAL_BYTES`] in their order, and counting the rest as + /// unobserved. `None` where not even an empty hint fits. + #[must_use] + pub fn encode_bounded( + deployment_id: &str, + target: Target, + items: &[ObservedItem], + ) -> Option> { + let mut request = Self { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: deployment_id.to_owned(), + target, + items: Vec::with_capacity(items.len()), + unobserved: 0, + }; + let mut encoded = serde_json::to_vec(&request).ok()?; + if encoded.len() > MAX_SIGNAL_BYTES { + return None; + } + for (index, item) in items.iter().enumerate() { + request.items.push(item.clone()); + request.unobserved = u32::try_from(items.len() - index - 1).unwrap_or(u32::MAX); + let next = serde_json::to_vec(&request).ok()?; + if next.len() > MAX_SIGNAL_BYTES { + request.items.pop(); + request.unobserved = u32::try_from(items.len() - index).unwrap_or(u32::MAX); + return serde_json::to_vec(&request).ok(); + } + encoded = next; + } + Some(encoded) + } +} + +/// One variant parameter a deployment admits, with every value it may take. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VariantParam { + /// The query parameter's name. + pub name: String, + /// Every value the parameter may take. + pub values: Vec, +} + +/// The bounded variant schema a deployment configures: which query +/// parameters name a variant, and with which values. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct VariantSchema(pub Vec); + +impl VariantSchema { + /// Whether the schema is within its bounds: at most + /// [`MAX_VARIANT_PARAMS`] distinct non-empty names, each with between + /// one and [`MAX_VARIANT_VALUES`] distinct values, every name and value + /// at most 64 URL-safe bytes. + #[must_use] + pub fn is_bounded(&self) -> bool { + let safe = |value: &str| { + !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + }; + let mut names = std::collections::BTreeSet::new(); + self.0.len() <= MAX_VARIANT_PARAMS + && self.0.iter().all(|param| { + let mut values = std::collections::BTreeSet::new(); + safe(¶m.name) + && names.insert(param.name.as_str()) + && !param.values.is_empty() + && param.values.len() <= MAX_VARIANT_VALUES + && param + .values + .iter() + .all(|value| safe(value) && values.insert(value.as_str())) + }) + } + + /// The canonical variant a reader's `query` names, its admitted pairs + /// sorted by name, or `None` where it carries anything the schema does + /// not admit: an unknown name, an unlisted value, a name given twice. An + /// empty query is the empty variant. + #[must_use] + pub fn admit(&self, query: &str) -> Option { + if query.is_empty() { + return Some(String::new()); + } + let mut pairs = BTreeMap::new(); + for (name, value) in url::form_urlencoded::parse(query.as_bytes()) { + let param = self.0.iter().find(|param| param.name == name)?; + if !param.values.iter().any(|allowed| *allowed == value) { + return None; + } + if pairs + .insert(param.name.clone(), value.into_owned()) + .is_some() + { + return None; + } + } + Some( + pairs + .into_iter() + .map(|(name, value)| format!("{name}={value}")) + .collect::>() + .join("&"), + ) + } +} + +/// The key a snapshot capability is signed with: derived from the +/// publication secret, so the secret itself never signs anything a +/// request carries. +fn capability_key(publication_secret: &str, purpose: &str) -> HmacSha256 { + let mut derive = HmacSha256::new_from_slice(publication_secret.as_bytes()) + .expect("should key an HMAC with any length"); + derive.update(b"encypher-preparation/1:"); + derive.update(purpose.as_bytes()); + let key = derive.finalize().into_bytes(); + HmacSha256::new_from_slice(&key).expect("should key an HMAC with a derived key") +} + +fn mac_hex(mut mac: HmacSha256, message: &[u8]) -> String { + mac.update(message); + hex_lower(&mac.finalize().into_bytes()) +} + +/// The capability a snapshot route grants its own loopback read of +/// `path_and_query`, valid until `expires` with this `nonce`. +#[must_use] +pub fn snapshot_capability( + publication_secret: &str, + path_and_query: &str, + nonce: &str, + expires: u64, +) -> String { + let message = format!("{path_and_query}\n{nonce}\n{expires}"); + let mac = mac_hex( + capability_key(publication_secret, "capability"), + message.as_bytes(), + ); + format!("v1.{expires}.{nonce}.{mac}") +} + +/// The nonce of a capability valid for `path_and_query` at `now`, or `None` +/// for any other value: malformed, expired, too far in the future, or not +/// signed with the publication secret. Compared in constant time. +#[must_use] +pub fn verify_snapshot_capability( + publication_secret: &str, + path_and_query: &str, + capability: &str, + now: u64, +) -> Option { + let mut parts = capability.splitn(4, '.'); + if parts.next()? != "v1" { + return None; + } + let expires: u64 = parts.next()?.parse().ok()?; + let nonce = parts.next()?; + let mac = parts.next()?; + if now > expires + || expires.saturating_sub(now) > CAPABILITY_LIFETIME_SECONDS + || nonce.len() != 32 + || !nonce.bytes().all(|byte| byte.is_ascii_hexdigit()) + { + return None; + } + let expected = snapshot_capability(publication_secret, path_and_query, nonce, expires); + let expected_mac = expected.rsplit('.').next()?; + bool::from(expected_mac.as_bytes().ct_eq(mac.as_bytes())).then(|| nonce.to_owned()) +} + +/// What the origin's own headers said about a snapshot, as the delivery +/// instance reports it, kept apart from the snapshot's own private +/// transport headers. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SnapshotOrigin { + /// The origin's status. + pub status: u16, + /// The origin marked its answer as one reader's. + pub private: bool, + /// The origin forbade transforming the body. + pub no_transform: bool, + /// The origin's own `Link` names a C2PA manifest. + pub manifest_link: bool, + /// The origin's `Content-Encoding`, empty for identity. + pub content_encoding: String, + /// The lower case host the source was fetched from. + pub origin_host: String, + /// SHA-256 of core's configured media-origin identity. Empty for HTML; + /// large-source admission requires a valid digest. + pub origin_identity_sha256: String, + /// The exact fetched address, where it carries no user information, + /// query or fragment. + pub source_reference: Option, +} + +/// The facts header for `origin`, authenticated for `nonce`. +#[must_use] +pub fn facts_header(publication_secret: &str, nonce: &str, origin: &SnapshotOrigin) -> String { + let body = serde_json::to_vec(origin).unwrap_or_default(); + let encoded = STANDARD.encode(&body); + let mut message = nonce.as_bytes().to_vec(); + message.push(b'\n'); + message.extend_from_slice(encoded.as_bytes()); + let mac = mac_hex(capability_key(publication_secret, "facts"), &message); + format!("v1.{encoded}.{mac}") +} + +/// The origin facts a header reports, where it is authenticated for +/// `nonce`; `None` for anything else, such as a header an origin wrote. +#[must_use] +pub fn verify_facts_header( + publication_secret: &str, + nonce: &str, + header: &str, +) -> Option { + let rest = header.strip_prefix("v1.")?; + let (encoded, mac) = rest.rsplit_once('.')?; + let mut message = nonce.as_bytes().to_vec(); + message.push(b'\n'); + message.extend_from_slice(encoded.as_bytes()); + let expected = mac_hex(capability_key(publication_secret, "facts"), &message); + if !bool::from(expected.as_bytes().ct_eq(mac.as_bytes())) { + return None; + } + serde_json::from_slice(&STANDARD.decode(encoded).ok()?).ok() +} + +/// Whether `presented` is `secret`, compared in constant time. An empty +/// secret authenticates nothing. +#[must_use] +pub fn token_matches(secret: &str, presented: &str) -> bool { + !secret.is_empty() && bool::from(secret.as_bytes().ct_eq(presented.as_bytes())) +} + +/// The idempotency key a text preparation sends, derived from its identity +/// and attempt generation so a retry of one operation replays it. +#[must_use] +pub fn operation_key(identity: &str, generation: u64) -> String { + domain_digest( + "encypher-preparation/1:operation", + &[identity.as_bytes(), generation.to_string().as_bytes()], + ) +} + +/// The response header carrying a snapshot's metadata, base64 JSON of +/// [`SnapshotMeta`]; the body is the snapshot's bytes. +pub const SNAPSHOT_META_HEADER: &str = "x-encypher-preparation-snapshot"; + +/// What a snapshot request reads. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)] +pub enum SnapshotMode { + /// The whole bounded representation. + Whole, + /// Headers only, for a source too large to hold whole. + Head, + /// One byte range of the version `if_match` names. + Range { + /// The first byte. + first: u64, + /// The last byte, inclusive. + last: u64, + /// The strong entity tag the range must come from. + if_match: String, + }, +} + +/// The worker's request for a snapshot of one configured target. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SnapshotRequest { + /// [`PREPARATION_PROTOCOL`]. + pub protocol: String, + /// The deployment. + pub deployment_id: String, + /// The target read, which must lie under a configured route with an + /// admitted variant. + pub target: Target, + /// 32 lower case hex characters the worker chose, which the facts the + /// snapshot reports are authenticated for. + pub nonce: String, + /// What is read. + pub read: SnapshotMode, +} + +/// What a snapshot answer reports besides its bytes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SnapshotMeta { + /// [`PREPARATION_PROTOCOL`]. + pub protocol: String, + /// The reader-facing address the snapshot was read at, without query. + pub page_url: String, + /// The representation's `Content-Type`. + pub content_type: String, + /// Its single strong entity tag, quoted, where it has one. + pub etag: Option, + /// Its total length, where known. + pub length: Option, + /// What the origin's own headers said, apart from the snapshot's own + /// private transport headers. + pub origin: SnapshotOrigin, +} + +/// One result the worker asks the delivery instance to verify and publish. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ResultEnvelope { + /// [`PREPARATION_PROTOCOL`]. + pub protocol: String, + /// The deployment. + pub deployment_id: String, + /// The identity the result is published under; the delivery instance + /// recomputes it from the item and refuses any other. + pub identity: String, + /// The retirement epoch the result was prepared under. + pub retirement_epoch: u64, + /// The admitted variant the item was read under. + #[serde(default)] + pub variant: String, + /// The item, as the delivery instance recovers and checks it. + pub item: EnvelopeItem, +} + +/// An item a [`ResultEnvelope`] carries. Never a signed result the worker +/// asserts: the delivery instance recovers each receipt from the API itself. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum EnvelopeItem { + /// An article: the exact sign request and its idempotency key. + Text { + /// The exact sign request. + request: super::pipeline::TextSignRequest, + /// Its idempotency key. + idempotency_key: String, + }, + /// A bounded standalone item with an external manifest. + Media { + /// The verified plan. + planned: super::pipeline::PlannedMedia, + /// The source bytes, base64, which the delivery instance hashes. + source_b64: String, + }, + /// An embedded item. + Embedded { + /// The verified plan. + planned: super::pipeline::PlannedMedia, + /// The source bytes, base64. + source_b64: String, + /// The worker's output bytes, base64, which must be exactly the + /// output the delivery instance builds from the receipt. + output_b64: String, + }, + /// A large item, named by its strong validator. + Large { + /// The verified plan. + planned: super::pipeline::PlannedMedia, + /// The strong entity tag the worker read every byte under. + etag: String, + /// Configured-origin identity observed in authenticated source facts. + origin_identity_sha256: String, + }, +} + +/// A retirement marker the worker asks the delivery instance to publish. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MarkerEnvelope { + /// [`PREPARATION_PROTOCOL`]. + pub protocol: String, + /// The deployment. + pub deployment_id: String, + /// The identity retired. + pub identity: String, + /// The retirement epoch. + pub epoch: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + + const SECRET: &str = "publication-secret-for-tests"; + + fn text(title: Option<&'static str>, variant: &'static str) -> TextIdentity<'static> { + TextIdentity { + credential_context: "0123456789abcdef", + policy_digest: "a", + page_url: "https://www.example.com/news/a", + variant, + text: "Harbor council votes", + document_title: title, + boundary_selector: "article", + } + } + + #[test] + fn identities_bind_every_part_and_never_collide_by_concatenation() { + let base = text(Some("Title"), "").digest(); + assert_eq!(base, text(Some("Title"), "").digest(), "should be stable"); + assert_ne!(base, text(None, "").digest(), "should bind title presence"); + assert_ne!( + base, + text(Some(""), "").digest(), + "should tell empty title from none" + ); + assert_ne!( + base, + text(Some("Title"), "edition=uk").digest(), + "should bind the variant" + ); + assert_ne!( + domain_digest("d", &[b"ab", b"c"]), + domain_digest("d", &[b"a", b"bc"]), + "length prefixes should separate parts" + ); + let media = MediaIdentity { + credential_context: "0123456789abcdef", + policy_digest: "a", + page_url: "https://www.example.com/news/a", + variant: "", + carrier: Carrier::Embedded, + mime: "image/png", + source_sha256: "b", + }; + let external = MediaIdentity { + carrier: Carrier::External, + ..media + }; + assert_ne!(media.digest(), external.digest(), "should bind the carrier"); + } + + #[test] + fn exact_request_text_identity_retires_the_coarse_cache_domain_only() { + let article = text(Some("Title"), ""); + let text_hash = sha256_hex(article.text.as_bytes()); + let parts = [ + article.credential_context.as_bytes(), + article.policy_digest.as_bytes(), + article.page_url.as_bytes(), + article.variant.as_bytes(), + TEXT_PROJECTION_VERSION.as_bytes(), + text_hash.as_bytes(), + &b"1"[..], + &b"Title"[..], + article.boundary_selector.as_bytes(), + ]; + assert_ne!( + article.digest(), + domain_digest("encypher-preparation/1:text", &parts), + "old coarse-cache prepared records must not satisfy a current text lookup" + ); + assert_eq!( + article.digest(), + domain_digest("encypher-preparation/2:text", &parts), + "the new domain versions the text cache contract, not the projection" + ); + let media = MediaIdentity { + credential_context: article.credential_context, + policy_digest: article.policy_digest, + page_url: article.page_url, + variant: article.variant, + carrier: Carrier::Embedded, + mime: "image/png", + source_sha256: "b", + }; + assert_eq!( + media.digest(), + domain_digest( + "encypher-preparation/1:media", + &[ + media.credential_context.as_bytes(), + media.policy_digest.as_bytes(), + media.page_url.as_bytes(), + media.variant.as_bytes(), + media.carrier.as_str().as_bytes(), + media.mime.as_bytes(), + media.source_sha256.as_bytes(), + ], + ), + "unchanged media must keep its existing prepared identity" + ); + } + + #[test] + fn capability_admits_only_its_own_path_secret_and_lifetime() { + let nonce = "0123456789abcdef0123456789abcdef"; + let capability = snapshot_capability(SECRET, "/news/a", nonce, 1_100); + assert_eq!( + verify_snapshot_capability(SECRET, "/news/a", &capability, 1_000).as_deref(), + Some(nonce), + "should admit a fresh capability for its path" + ); + assert!( + verify_snapshot_capability(SECRET, "/news/b", &capability, 1_000).is_none(), + "should refuse another path" + ); + assert!( + verify_snapshot_capability("other-secret", "/news/a", &capability, 1_000).is_none(), + "should refuse another secret" + ); + assert!( + verify_snapshot_capability(SECRET, "/news/a", &capability, 1_101).is_none(), + "should refuse an expired capability" + ); + let far = snapshot_capability(SECRET, "/news/a", nonce, 10_000); + assert!( + verify_snapshot_capability(SECRET, "/news/a", &far, 1_000).is_none(), + "should refuse a capability valid for longer than its lifetime" + ); + } + + #[test] + fn facts_header_verifies_only_for_its_nonce_and_secret() { + let origin = SnapshotOrigin { + status: 200, + private: true, + origin_host: "origin.example.com".to_owned(), + ..SnapshotOrigin::default() + }; + let header = facts_header(SECRET, "n1", &origin); + assert_eq!( + verify_facts_header(SECRET, "n1", &header), + Some(origin), + "should read back the facts" + ); + assert!( + verify_facts_header(SECRET, "n2", &header).is_none(), + "should refuse another nonce" + ); + assert!( + verify_facts_header("other", "n1", &header).is_none(), + "should refuse another secret, as an origin-written header" + ); + } + + #[test] + fn variant_schema_admits_only_bounded_listed_pairs() { + let schema = VariantSchema(vec![ + VariantParam { + name: "edition".to_owned(), + values: vec!["us".to_owned(), "uk".to_owned()], + }, + VariantParam { + name: "amp".to_owned(), + values: vec!["1".to_owned()], + }, + ]); + assert!(schema.is_bounded(), "should be within bounds"); + assert_eq!(schema.admit("").as_deref(), Some("")); + assert_eq!( + schema.admit("edition=uk&=1").as_deref(), + Some("amp=1&edition=uk"), + "should sort admitted pairs" + ); + assert!( + schema.admit("edition=fr").is_none(), + "should refuse an unlisted value" + ); + assert!( + schema.admit("utm_source=x").is_none(), + "should refuse an unknown name" + ); + assert!( + schema.admit("edition=us&edition=uk").is_none(), + "should refuse a repeated name" + ); + let unbounded = VariantSchema(vec![VariantParam { + name: "q".to_owned(), + values: Vec::new(), + }]); + assert!(!unbounded.is_bounded(), "should refuse an open parameter"); + } + + #[test] + fn bounded_signal_keeps_items_in_order_and_counts_the_rest() { + let items: Vec = (0..400) + .map(|index| ObservedItem { + kind: ItemKind::Embedded, + identity: format!("{index:064}"), + }) + .collect(); + let target = Target { + path: "/news/a".to_owned(), + variant: String::new(), + }; + let encoded = + SignalRequest::encode_bounded("deployment", target, &items).expect("should encode"); + assert!(encoded.len() <= MAX_SIGNAL_BYTES, "should fit the bound"); + let decoded: SignalRequest = serde_json::from_slice(&encoded).expect("should decode"); + assert!(!decoded.items.is_empty(), "should carry some items"); + assert_eq!( + decoded.items[0], items[0], + "should keep the first item first" + ); + assert_eq!( + decoded.items.len() + usize::try_from(decoded.unobserved).expect("should fit"), + items.len(), + "should count every item that did not fit" + ); + } + + #[test] + fn target_refuses_queries_traversal_and_relative_paths() { + let ok = Target { + path: "/news/a".to_owned(), + variant: String::new(), + }; + assert!(ok.is_well_formed()); + for path in [ + "news/a", + "//evil.example.com/a", + "/a?b=1", + "/a/../b", + "/a#b", + ] { + let target = Target { + path: path.to_owned(), + variant: String::new(), + }; + assert!(!target.is_well_formed(), "should refuse {path}"); + } + } +} diff --git a/crates/provenance/encypher/src/prepared_probe.rs b/crates/provenance/encypher/src/prepared_probe.rs new file mode 100644 index 0000000000..a4072a921d --- /dev/null +++ b/crates/provenance/encypher/src/prepared_probe.rs @@ -0,0 +1,995 @@ +//! The signed probe release, run on the edge over an asynchronous source. +//! +//! Large media is signed in prepared-hash mode: Encypher's planner decides +//! which structure bytes a manifest binds and how the carrier is built, and +//! the edge never holds the whole file. The planner's format logic ships only +//! inside Encypher's signed probe release, a WebAssembly artifact the signing +//! service advertises and serves. This module is the generic host for that +//! artifact. It holds no format, planner or hashing algorithm of its own: it +//! verifies a release, runs the operations the release implements (`extract`, +//! `verify_plan`, `build_carrier`, `binding_items`, `check_store` and the +//! rest), and returns the JSON the release answers with. +//! +//! # Release trust +//! +//! A release is run only after every check the advertised release contract +//! names, in order: the descriptor's Ed25519 signature (`verify_strict` over +//! its canonical bytes) by a key this build pins, a release that is not +//! revoked, ABI 1, a release at or above the floor, an artifact of at most +//! 4 MiB whose SHA-256 is the descriptor's, a module that imports exactly +//! `env.host_read` and exports the probe ABI, a `probe_abi` of 1, and a +//! `version` that reports the descriptor's `planner_id`. The caller supplies +//! the artifact bytes, fetched through the runtime's own HTTP client and +//! caches from [`ProbeDescriptor::artifact_path`]; nothing here touches the +//! network. +//! +//! # Asynchronous reads +//! +//! The release's only way to see bytes is its one import, +//! `host_read(offset, len, dst) -> status`, which the release calls +//! synchronously. On the edge a read is an HTTP range request, so it cannot +//! complete inside the interpreter's callback without blocking the executor. +//! The callback therefore checks the request against the source size and the +//! per-read bound and, when it is admissible, stops the interpreter with a +//! resumable host trap. [`ProbeModule::call`] then awaits the source outside +//! the interpreter, writing straight into the release's own read buffer in +//! linear memory, and resumes the suspended call with the status. The +//! operation is never restarted, so each read happens once, in order, and no +//! host-side copy of the file exists. +//! +//! Each call runs in a fresh instance with its own fuel, memory, per-read, +//! total-read and result bounds, so an abandoned call leaves nothing behind +//! and one [`ProbeModule`] serves concurrent calls. + +use std::convert::Infallible; +use std::fmt; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use ed25519_dalek::{Signature, VerifyingKey}; +use serde_json::{Value, json}; +use wasmi::{ + Caller, Config, Engine, ExternType, Linker, Memory, Module, Store, StoreLimits, + StoreLimitsBuilder, TrapCode, TypedFunc, TypedResumableCall, Val, ValType, +}; + +use crate::media_api::sha256_hex; + +/// The probe host ABI this host implements. +pub(crate) const PROBE_ABI: u64 = 1; + +/// Encypher's production probe release key: a raw Ed25519 public key, lower +/// case hex `9ae0bf4a...6b75d9`. Pinning it means a release Encypher did not +/// sign is refused even when the API or a cache serving the artifact is +/// compromised. +const PRODUCTION_RELEASE_KEY: [u8; 32] = [ + 0x9a, 0xe0, 0xbf, 0x4a, 0xea, 0x51, 0x40, 0x00, 0x8d, 0x2f, 0x48, 0xe6, 0xc0, 0x86, 0x91, 0x36, + 0x0a, 0x7f, 0xe2, 0x3e, 0x0d, 0x36, 0xf2, 0x45, 0x22, 0xc7, 0x70, 0xb7, 0x64, 0x6b, 0x75, 0xd9, +]; + +/// The lowest probe release this build runs. Raise it when Encypher +/// withdraws a release, so a compromised API cannot downgrade the edge to it. +const PRODUCTION_RELEASE_FLOOR: u64 = 8; + +/// A release artifact is about 800 KB; anything over this is not one. +const MAX_ARTIFACT_BYTES: usize = 4 * 1024 * 1024; + +/// `host_read` succeeded and the destination holds the requested bytes. +const READ_OK: i32 = 0; +/// `host_read` named a destination outside the release's linear memory. +const READ_MEMORY_OUT_OF_BOUNDS: i32 = 4; +/// `host_read` asked for more than one read may carry or for bytes past the +/// end of the source. +const READ_OUT_OF_BOUNDS: i32 = 5; + +/// A signed release descriptor, as the capabilities response and a 409 +/// refusal carry it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ProbeDescriptor { + pub(crate) abi: u64, + pub(crate) planner_id: String, + pub(crate) release: u64, + pub(crate) sha256: String, + pub(crate) signature: String, +} + +impl ProbeDescriptor { + /// Reads one descriptor, or `None` when its shape is not the contract's: + /// unsigned integers `abi` and `release`, lower case 64-hex `planner_id` + /// and `sha256`, and a non-empty `signature`. + #[must_use] + pub(crate) fn parse(value: &Value) -> Option { + let hex64 = |text: &&str| { + text.len() == 64 + && text + .bytes() + .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + }; + Some(Self { + abi: value.get("abi")?.as_u64()?, + planner_id: value.get("planner_id")?.as_str().filter(hex64)?.to_owned(), + release: value.get("release")?.as_u64()?, + sha256: value.get("sha256")?.as_str().filter(hex64)?.to_owned(), + signature: value + .get("signature")? + .as_str() + .filter(|text| !text.is_empty())? + .to_owned(), + }) + } + + /// Every well-formed descriptor in a `probe` array; malformed entries + /// are dropped, and anything other than an array is none. + #[must_use] + pub(crate) fn parse_list(value: Option<&Value>) -> Vec { + value + .and_then(Value::as_array) + .map(|items| items.iter().filter_map(Self::parse).collect()) + .unwrap_or_default() + } + + #[cfg(test)] + /// The descriptor as the signing API receives it back. + #[must_use] + pub(crate) fn to_json(&self) -> Value { + json!({ + "abi": self.abi, + "planner_id": self.planner_id, + "release": self.release, + "sha256": self.sha256, + "signature": self.signature, + }) + } + + /// The exact bytes the release key signs: the four signed fields, keys + /// sorted, no whitespace. + #[must_use] + pub(crate) fn canonical_json(&self) -> String { + format!( + "{{\"abi\":{},\"planner_id\":\"{}\",\"release\":{},\"sha256\":\"{}\"}}", + self.abi, self.planner_id, self.release, self.sha256 + ) + } + + /// The signing API path that serves this release's artifact, relative to + /// the API base. + #[must_use] + pub(crate) fn artifact_path(&self) -> String { + format!("sign/media/probe/{}.wasm", self.sha256) + } +} + +/// The release keys, floor and revocations a release must satisfy. +/// +/// Production code obtains only [`ReleaseAuthority::production`]: the keys +/// are compiled in and nothing a configuration or a request carries can +/// change them. +#[derive(Debug, Clone)] +pub(crate) struct ReleaseAuthority { + keys: Vec, + floor: u64, + /// Lower case artifact SHA-256s withdrawn for cause. + revoked: Vec, +} + +impl ReleaseAuthority { + /// Encypher's production release key and this build's floor. + /// + /// # Panics + /// + /// Never: the pinned key is a valid Ed25519 point, which a test proves. + #[must_use] + pub(crate) fn production() -> Self { + let key = VerifyingKey::from_bytes(&PRODUCTION_RELEASE_KEY) + .expect("should decode the pinned production release key"); + Self { + keys: vec![key], + floor: PRODUCTION_RELEASE_FLOOR, + revoked: Vec::new(), + } + } + + /// An authority over test keys, for tests only. + #[cfg(test)] + pub(crate) fn pinned(keys: Vec, floor: u64, revoked: Vec) -> Self { + Self { + keys, + floor, + revoked, + } + } + + /// Checks a descriptor before any of its release's bytes are fetched: + /// revocation, signature, ABI and floor. + /// + /// # Errors + /// + /// The [`ReleaseError`] naming the first check the descriptor fails. + pub(crate) fn verify(&self, descriptor: &ProbeDescriptor) -> Result<(), ReleaseError> { + if self + .revoked + .iter() + .any(|sha256| sha256.eq_ignore_ascii_case(&descriptor.sha256)) + { + return Err(ReleaseError::Revoked { + release: descriptor.release, + }); + } + let signature = STANDARD + .decode(&descriptor.signature) + .ok() + .and_then(|bytes| Signature::from_slice(&bytes).ok()) + .ok_or(ReleaseError::MalformedSignature { + release: descriptor.release, + })?; + let message = descriptor.canonical_json(); + let signed = self + .keys + .iter() + .any(|key| key.verify_strict(message.as_bytes(), &signature).is_ok()); + if !signed { + return Err(ReleaseError::Unsigned { + release: descriptor.release, + }); + } + if descriptor.abi != PROBE_ABI { + return Err(ReleaseError::AbiUnsupported { + abi: descriptor.abi, + }); + } + if descriptor.release < self.floor { + return Err(ReleaseError::BelowFloor { + release: descriptor.release, + floor: self.floor, + }); + } + Ok(()) + } + + /// The newest advertised release this host implements the ABI of, + /// verified. + /// + /// # Errors + /// + /// [`ReleaseError::NoneAdvertised`] for an empty list, + /// [`ReleaseError::NoCompatibleRelease`] when none uses this ABI, and the + /// chosen descriptor's verification failure otherwise. An older release + /// is never chosen in place of a newer one that fails. + pub(crate) fn select<'a>( + &self, + descriptors: &'a [ProbeDescriptor], + ) -> Result<&'a ProbeDescriptor, ReleaseError> { + if descriptors.is_empty() { + return Err(ReleaseError::NoneAdvertised); + } + let descriptor = descriptors + .iter() + .filter(|descriptor| descriptor.abi == PROBE_ABI) + .max_by_key(|descriptor| descriptor.release) + .ok_or(ReleaseError::NoCompatibleRelease)?; + self.verify(descriptor)?; + Ok(descriptor) + } +} + +/// Why a release was not run. Every variant is refused before the release +/// reads a byte of any source. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ReleaseError { + /// The service advertised no release. + NoneAdvertised, + /// The service advertised no release for [`PROBE_ABI`]. + NoCompatibleRelease, + /// The release was withdrawn for cause. + Revoked { release: u64 }, + /// The signature is not 64 bytes of standard padded base64. + MalformedSignature { release: u64 }, + /// No pinned key signed the descriptor. + Unsigned { release: u64 }, + /// The descriptor names an ABI this host does not implement. + AbiUnsupported { abi: u64 }, + /// The release is older than this build's floor. + BelowFloor { release: u64, floor: u64 }, + /// The artifact is larger than any release. + ArtifactTooLarge { bytes: usize }, + /// The artifact does not hash to the signed digest. + DigestMismatch, + /// The artifact is not a valid WebAssembly module. + InvalidModule(String), + /// The module imports anything other than exactly `env.host_read`. + ImportsMismatch, + /// The module does not export the probe ABI, or its start or + /// `probe_abi` trapped. + ExportsMismatch(String), + /// `probe_abi` reports an ABI other than the descriptor's. + AbiReported { reported: u32 }, + /// `version` failed or did not report the descriptor's `planner_id`. + PlannerMismatch, +} + +impl ReleaseError { + /// The client contract's code for this refusal. + #[must_use] + pub(crate) fn code(&self) -> &'static str { + match self { + Self::NoneAdvertised => "PROBE_RELEASE_UNAVAILABLE", + Self::NoCompatibleRelease | Self::AbiUnsupported { .. } => "PROBE_UPGRADE_REQUIRED", + _ => "PROBE_RELEASE_INVALID", + } + } +} + +impl fmt::Display for ReleaseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoneAdvertised => { + formatter.write_str("the signing service advertised no probe release") + } + Self::NoCompatibleRelease => write!( + formatter, + "the signing service advertised no probe release for ABI {PROBE_ABI}" + ), + Self::Revoked { release } => { + write!(formatter, "probe release {release} was withdrawn") + } + Self::MalformedSignature { release } => { + write!( + formatter, + "probe release {release} carries a malformed signature" + ) + } + Self::Unsigned { release } => write!( + formatter, + "probe release {release} is not signed by a pinned release key" + ), + Self::AbiUnsupported { abi } => write!( + formatter, + "the probe release uses ABI {abi}; this host implements ABI {PROBE_ABI}" + ), + Self::BelowFloor { release, floor } => write!( + formatter, + "probe release {release} is below this build's floor of {floor}" + ), + Self::ArtifactTooLarge { bytes } => write!( + formatter, + "the probe artifact is {bytes} bytes, over the {MAX_ARTIFACT_BYTES} byte limit" + ), + Self::DigestMismatch => { + formatter.write_str("the probe artifact does not hash to its signed digest") + } + Self::InvalidModule(error) => write!( + formatter, + "the probe artifact is not a valid WebAssembly module: {error}" + ), + Self::ImportsMismatch => { + formatter.write_str("the probe release imports something other than env.host_read") + } + Self::ExportsMismatch(error) => write!( + formatter, + "the probe release does not implement the host ABI: {error}" + ), + Self::AbiReported { reported } => write!( + formatter, + "the probe release reports ABI {reported}, not its descriptor's" + ), + Self::PlannerMismatch => { + formatter.write_str("the probe release does not report its descriptor's planner") + } + } + } +} + +impl std::error::Error for ReleaseError {} + +/// Bounds on one call. +/// +/// Fuel, the total read and the result abandon the call when exceeded. A +/// read over the per-read bound is answered with status 5, and growth past +/// the memory bound fails inside the release, which then answers as it +/// handles an allocation failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProbeLimits { + /// Linear memory one instance may grow to. + pub(crate) memory_bytes: usize, + /// Instructions one call may execute. + pub(crate) fuel_per_call: u64, + /// The largest single read; a larger request is answered with status 5. + pub(crate) max_read_bytes: u32, + /// Bytes all reads of one call may carry together. + pub(crate) max_total_read_bytes: u64, + /// The largest result a call may return. + pub(crate) max_result_bytes: u32, +} + +impl Default for ProbeLimits { + /// Leave room for the host, parsed JSON and the signed module on + /// 128 MiB-class runtimes. Media payload hashing stays outside WASM. + /// Larger structure or result disclosures are refused explicitly. + fn default() -> Self { + Self { + memory_bytes: 32 * 1024 * 1024, + fuel_per_call: 200_000_000, + max_read_bytes: 8 * 1024 * 1024, + max_total_read_bytes: 64 * 1024 * 1024, + max_result_bytes: 4 * 1024 * 1024, + } + } +} + +/// One version of one file, read at offsets. +/// +/// An implementation MUST serve every read of a call from the same version +/// (an HTTP source pins a strong `ETag` and refuses a range answered from +/// any other) and MUST fail rather than return fewer bytes than asked for. +pub(crate) trait ProbeSource { + /// Why a read failed. The call is abandoned and the error returned as + /// [`ProbeCallError::Source`]. + type Error; + + /// The size of the pinned version, in bytes. The host refuses any read + /// past it before the source is asked. + fn size(&self) -> u64; + + /// Fills `destination` with the bytes at `offset`. + fn read_exact_at( + &self, + offset: u64, + destination: &mut [u8], + ) -> impl Future>; +} + +/// The source of an operation that reads no file, such as `version`, +/// `build_carrier` or `check_store`. It has no bytes, so the host refuses +/// every non-empty read with status 5. +struct NoSource; + +impl ProbeSource for NoSource { + type Error = Infallible; + + fn size(&self) -> u64 { + 0 + } + + async fn read_exact_at(&self, _offset: u64, _destination: &mut [u8]) -> Result<(), Infallible> { + Ok(()) + } +} + +/// The release's own error object, returned with status 1. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ProbeRefusal { + /// The probe's code, such as `PLAN_MISMATCH` or `UNSUPPORTED_FORMAT`. + pub(crate) code: String, + /// Whether the refusal reports an integrity failure. + pub(crate) integrity: bool, + pub(crate) message: String, +} + +/// Why a call ended without the operation's value. +#[derive(Debug)] +pub(crate) enum ProbeCallError { + /// The source failed a read. The suspended call was dropped. + Source(E), + /// The release answered with its error object. + Refused(ProbeRefusal), + /// The host stopped the call, or the release broke the ABI. + Failed(ProbeFailure), +} + +impl ProbeCallError { + /// The client contract's code: the release's own code for a refusal, + /// `PROBE_FAILED` otherwise. + #[must_use] + pub(crate) fn code(&self) -> &str { + match self { + Self::Refused(refusal) => &refusal.code, + Self::Source(_) | Self::Failed(_) => "PROBE_FAILED", + } + } +} + +impl fmt::Display for ProbeCallError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Source(error) => write!(formatter, "the probe's source failed a read: {error}"), + Self::Refused(refusal) => { + write!( + formatter, + "the probe refused: {}: {}", + refusal.code, refusal.message + ) + } + Self::Failed(failure) => failure.fmt(formatter), + } + } +} + +impl std::error::Error for ProbeCallError {} + +impl ProbeCallError { + /// The same error over any source error type, for a call that reads no + /// source. + fn widen(self) -> ProbeCallError { + match self { + Self::Source(never) => match never {}, + Self::Refused(refusal) => ProbeCallError::Refused(refusal), + Self::Failed(failure) => ProbeCallError::Failed(failure), + } + } +} + +/// How the host stopped a call, or how the release broke the ABI during it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ProbeFailure { + /// The instance could not be created or lacked an export. + Instantiate(String), + /// The operation name or argument does not fit a 32-bit length. + ArgumentTooLarge, + /// The release trapped, or a host call failed outside `host_read`. + Trap(String), + /// The call used its fuel. + FuelExhausted, + /// The call's reads together exceeded the total read bound. + ReadBudgetExceeded { budget: u64 }, + /// `probe_call` returned a status other than 0 or 1. + UnknownStatus(i32), + /// The result is larger than the result bound. + ResultTooLarge { bytes: u32, limit: u32 }, + /// The result lies outside linear memory. + ResultOutOfBounds, + /// The result is not JSON, or a status 1 result is not an error object. + MalformedResult(String), +} + +impl ProbeFailure { + /// An interpreter error: fuel exhaustion, or any other trap. + fn stopped(error: &wasmi::Error) -> Self { + if error.as_trap_code() == Some(TrapCode::OutOfFuel) { + Self::FuelExhausted + } else { + Self::Trap(error.to_string()) + } + } +} + +impl fmt::Display for ProbeFailure { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Instantiate(error) => { + write!( + formatter, + "the probe release could not be instantiated: {error}" + ) + } + Self::ArgumentTooLarge => { + formatter.write_str("a probe argument exceeds a 32-bit length") + } + Self::Trap(error) => write!( + formatter, + "the probe release stopped with an error: {error}" + ), + Self::FuelExhausted => formatter.write_str("the probe release ran out of fuel"), + Self::ReadBudgetExceeded { budget } => write!( + formatter, + "the probe release read more than its {budget} byte budget" + ), + Self::UnknownStatus(status) => { + write!( + formatter, + "the probe release returned unknown status {status}" + ) + } + Self::ResultTooLarge { bytes, limit } => write!( + formatter, + "the probe release returned {bytes} bytes, over the {limit} byte limit" + ), + Self::ResultOutOfBounds => { + formatter.write_str("the probe release's result lies outside its memory") + } + Self::MalformedResult(error) => { + write!( + formatter, + "the probe release returned a malformed result: {error}" + ) + } + } + } +} + +/// A verified, compiled release. +pub(crate) struct ProbeModule { + module: Module, + linker: Linker, + descriptor: ProbeDescriptor, + limits: ProbeLimits, +} + +impl fmt::Debug for ProbeModule { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ProbeModule") + .field("descriptor", &self.descriptor) + .field("limits", &self.limits) + .finish_non_exhaustive() + } +} + +impl ProbeModule { + /// Verifies `artifact` as the release `descriptor` names and compiles it. + /// + /// The descriptor is checked against `authority` first, so an unsigned + /// artifact is never parsed. The release's `version` runs once, with no + /// source, to confirm its planner. + /// + /// # Errors + /// + /// The [`ReleaseError`] naming the first check that failed. + pub(crate) async fn load( + authority: &ReleaseAuthority, + descriptor: &ProbeDescriptor, + artifact: &[u8], + limits: ProbeLimits, + ) -> Result { + authority.verify(descriptor)?; + if artifact.len() > MAX_ARTIFACT_BYTES { + return Err(ReleaseError::ArtifactTooLarge { + bytes: artifact.len(), + }); + } + if sha256_hex(artifact) != descriptor.sha256 { + return Err(ReleaseError::DigestMismatch); + } + let mut config = Config::default(); + config.consume_fuel(true); + let engine = Engine::new(&config); + let module = Module::new(&engine, artifact) + .map_err(|error| ReleaseError::InvalidModule(error.to_string()))?; + if !imports_only_host_read(&module) { + return Err(ReleaseError::ImportsMismatch); + } + let mut linker = Linker::::new(&engine); + linker + .func_wrap("env", "host_read", host_read) + .map_err(|error| ReleaseError::ExportsMismatch(error.to_string()))?; + let probe = Self { + module, + linker, + descriptor: descriptor.clone(), + limits, + }; + + let mut instance = probe + .instantiate(0) + .map_err(|error| ReleaseError::ExportsMismatch(error.to_string()))?; + let reported = instance + .abi + .call(&mut instance.store, ()) + .map_err(|error| ReleaseError::ExportsMismatch(error.to_string()))?; + if u64::from(reported) != descriptor.abi { + return Err(ReleaseError::AbiReported { reported }); + } + let version = probe + .call_without_source::("version", &json!({})) + .await + .map_err(|_| ReleaseError::PlannerMismatch)?; + if version.get("planner_id").and_then(Value::as_str) != Some(descriptor.planner_id.as_str()) + { + return Err(ReleaseError::PlannerMismatch); + } + Ok(probe) + } + + /// The verified descriptor of this release. + #[must_use] + pub(crate) fn descriptor(&self) -> &ProbeDescriptor { + &self.descriptor + } + + /// Runs `op` with `arg` over `source` and returns the release's value. + /// + /// The operation name is passed through, so every operation the release + /// implements is available. Each read the release makes is awaited from + /// `source` outside the interpreter and the suspended call resumed. + /// + /// # Errors + /// + /// [`ProbeCallError::Source`] when a read fails, + /// [`ProbeCallError::Refused`] when the release answers with its error + /// object, and [`ProbeCallError::Failed`] when a bound stops the call or + /// the release breaks the ABI. + pub(crate) async fn call( + &self, + op: &str, + arg: &Value, + source: &S, + ) -> Result> { + let mut instance = self.instantiate(source.size()).map_err(|error| { + ProbeCallError::Failed(ProbeFailure::Instantiate(error.to_string())) + })?; + let (op_pointer, op_length) = instance + .put(op.as_bytes()) + .map_err(ProbeCallError::Failed)?; + let (arg_pointer, arg_length) = instance + .put(arg.to_string().as_bytes()) + .map_err(ProbeCallError::Failed)?; + + let mut total_read = 0_u64; + let mut state = instance + .call + .call_resumable( + &mut instance.store, + (op_pointer, op_length, arg_pointer, arg_length), + ) + .map_err(stopped)?; + let status = loop { + match state { + TypedResumableCall::Finished(status) => break status, + TypedResumableCall::OutOfFuel(_) => { + return Err(ProbeCallError::Failed(ProbeFailure::FuelExhausted)); + } + TypedResumableCall::HostTrap(suspended) => { + // `host_read` is the only import, so its request is the + // only host error a call can stop with. + let Some(request) = suspended + .host_error() + .downcast_ref::() + .copied() + else { + return Err(ProbeCallError::Failed(ProbeFailure::Trap( + suspended.host_error().to_string(), + ))); + }; + let budget = self.limits.max_total_read_bytes; + total_read = total_read + .checked_add(u64::from(request.length)) + .filter(|total| *total <= budget) + .ok_or(ProbeCallError::Failed(ProbeFailure::ReadBudgetExceeded { + budget, + }))?; + let status = instance.serve(request, source).await?; + state = suspended + .resume(&mut instance.store, &[Val::I32(status)]) + .map_err(stopped)?; + } + } + }; + let result = instance + .result(self.limits.max_result_bytes) + .map_err(ProbeCallError::Failed)?; + let value: Value = serde_json::from_slice(&result).map_err(|error| { + ProbeCallError::Failed(ProbeFailure::MalformedResult(error.to_string())) + })?; + match status { + 0 => Ok(value), + 1 => Err(ProbeCallError::Refused( + refusal(&value).map_err(ProbeCallError::Failed)?, + )), + other => Err(ProbeCallError::Failed(ProbeFailure::UnknownStatus(other))), + } + } + + /// Runs `op` with `arg` and no source, for operations that read no file. + /// + /// # Errors + /// + /// As [`ProbeModule::call`], without a source error. + pub(crate) async fn call_without_source( + &self, + op: &str, + arg: &Value, + ) -> Result> { + self.call(op, arg, &NoSource) + .await + .map_err(ProbeCallError::widen) + } + + /// A fresh instance whose single import reads a source of `source_size` + /// bytes. + fn instantiate(&self, source_size: u64) -> Result { + let mut store = Store::new( + self.module.engine(), + HostState { + limits: StoreLimitsBuilder::new() + .memory_size(self.limits.memory_bytes) + .instances(1) + .memories(1) + .tables(4) + .build(), + source_size, + max_read_bytes: self.limits.max_read_bytes, + }, + ); + store.limiter(|state| &mut state.limits); + store.set_fuel(self.limits.fuel_per_call)?; + let instance = self + .linker + .instantiate_and_start(&mut store, &self.module)?; + let memory = instance + .get_memory(&store, "memory") + .ok_or_else(|| wasmi::Error::new("the release exports no memory"))?; + let _free: TypedFunc<(u32, u32), ()> = instance.get_typed_func(&store, "probe_free")?; + Ok(Instance { + abi: instance.get_typed_func(&store, "probe_abi")?, + alloc: instance.get_typed_func(&store, "probe_alloc")?, + call: instance.get_typed_func(&store, "probe_call")?, + result_ptr: instance.get_typed_func(&store, "probe_result_ptr")?, + result_len: instance.get_typed_func(&store, "probe_result_len")?, + memory, + store, + }) + } +} + +/// Whether the module's only import is `env.host_read(i64, i32, i32) -> +/// i32`. +fn imports_only_host_read(module: &Module) -> bool { + let mut imports = module.imports(); + let Some(import) = imports.next() else { + return false; + }; + let host_read = import.module() == "env" + && import.name() == "host_read" + && matches!( + import.ty(), + ExternType::Func(ty) + if ty.params() == [ValType::I64, ValType::I32, ValType::I32] + && ty.results() == [ValType::I32] + ); + host_read && imports.next().is_none() +} + +/// The release's error object, from a status 1 result. +fn refusal(value: &Value) -> Result { + let error = value + .get("error") + .ok_or_else(|| ProbeFailure::MalformedResult("a status 1 result has no error".into()))?; + let text = |field: &str| { + error + .get(field) + .and_then(Value::as_str) + .map(str::to_owned) + .ok_or_else(|| ProbeFailure::MalformedResult(format!("the error has no {field}"))) + }; + Ok(ProbeRefusal { + code: text("code")?, + integrity: error + .get("integrity") + .and_then(Value::as_bool) + .unwrap_or(false), + message: text("message")?, + }) +} + +/// A call stopped by the interpreter: out of fuel, or any other trap. +#[allow( + clippy::needless_pass_by_value, + reason = "used directly as a Result error mapper" +)] +fn stopped(error: wasmi::Error) -> ProbeCallError { + ProbeCallError::Failed(ProbeFailure::stopped(&error)) +} + +/// What the store holds for the single import. +struct HostState { + limits: StoreLimits, + source_size: u64, + max_read_bytes: u32, +} + +/// An admissible read, carried out of the interpreter as a resumable host +/// trap. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ReadRequest { + offset: u64, + length: u32, + destination: u32, +} + +impl fmt::Display for ReadRequest { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "read of {} bytes at offset {} into {}", + self.length, self.offset, self.destination + ) + } +} + +impl wasmi::errors::HostError for ReadRequest {} + +/// `env.host_read`. Refuses a read over the per-read bound or past the +/// source with status 5, and suspends the call for any other. +#[allow( + clippy::needless_pass_by_value, + reason = "wasmi's host import ABI owns its Caller" +)] +fn host_read( + caller: Caller<'_, HostState>, + offset: u64, + length: u32, + destination: u32, +) -> Result { + let state = caller.data(); + let in_bounds = length <= state.max_read_bytes + && offset + .checked_add(u64::from(length)) + .is_some_and(|end| end <= state.source_size); + if !in_bounds { + return Ok(READ_OUT_OF_BOUNDS); + } + Err(wasmi::Error::host(ReadRequest { + offset, + length, + destination, + })) +} + +/// One instance, for one call. +struct Instance { + store: Store, + memory: Memory, + abi: TypedFunc<(), u32>, + alloc: TypedFunc, + call: TypedFunc<(u32, u32, u32, u32), i32>, + result_ptr: TypedFunc<(), u32>, + result_len: TypedFunc<(), u32>, +} + +impl Instance { + /// Copies `bytes` into a buffer the release allocates. + fn put(&mut self, bytes: &[u8]) -> Result<(u32, u32), ProbeFailure> { + let length = u32::try_from(bytes.len()).map_err(|_| ProbeFailure::ArgumentTooLarge)?; + let pointer = self + .alloc + .call(&mut self.store, length) + .map_err(|error| ProbeFailure::stopped(&error))?; + self.memory + .write(&mut self.store, pointer as usize, bytes) + .map_err(|error| ProbeFailure::Trap(error.to_string()))?; + Ok((pointer, length)) + } + + /// Awaits one read from `source` straight into the release's buffer and + /// returns the status to resume with. + async fn serve( + &mut self, + request: ReadRequest, + source: &S, + ) -> Result> { + let start = request.destination as usize; + let memory = self.memory.data_mut(&mut self.store); + let destination = match start.checked_add(request.length as usize) { + Some(end) => memory.get_mut(start..end), + None => None, + }; + let Some(destination) = destination else { + return Ok(READ_MEMORY_OUT_OF_BOUNDS); + }; + source + .read_exact_at(request.offset, destination) + .await + .map_err(ProbeCallError::Source)?; + Ok(READ_OK) + } + + /// The finished call's result bytes, bounded. + fn result(&mut self, limit: u32) -> Result, ProbeFailure> { + let trap = |error: wasmi::Error| ProbeFailure::stopped(&error); + let pointer = self.result_ptr.call(&mut self.store, ()).map_err(trap)?; + let length = self.result_len.call(&mut self.store, ()).map_err(trap)?; + if length > limit { + return Err(ProbeFailure::ResultTooLarge { + bytes: length, + limit, + }); + } + let mut bytes = vec![0_u8; length as usize]; + self.memory + .read(&self.store, pointer as usize, &mut bytes) + .map_err(|_| ProbeFailure::ResultOutOfBounds)?; + Ok(bytes) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/provenance/encypher/src/prepared_probe/tests.rs b/crates/provenance/encypher/src/prepared_probe/tests.rs new file mode 100644 index 0000000000..a9516890d3 --- /dev/null +++ b/crates/provenance/encypher/src/prepared_probe/tests.rs @@ -0,0 +1,766 @@ +//! The probe host against a stand-in release written in WebAssembly text. +//! +//! The stand-in implements the probe ABI with no format logic: its +//! operations exercise the host contract (two sequential reads, refused +//! reads, a read budget, fuel, memory growth, an oversized result, a trap and +//! the error object), so no Encypher planner code enters this repository. + +use std::cell::{Cell, RefCell}; +use std::fmt; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use ed25519_dalek::{Signer as _, SigningKey}; +use futures::executor::block_on; +use serde_json::json; + +use super::{ + MAX_ARTIFACT_BYTES, ProbeCallError, ProbeDescriptor, ProbeFailure, ProbeLimits, ProbeModule, + ProbeSource, ReleaseAuthority, ReleaseError, +}; +use crate::media_api::sha256_hex; + +const READ_JSON: &str = r#"{"head":"____","next":"____"}"#; +const STATUS_JSON: &str = r#"{"status":0}"#; +const UNKNOWN_JSON: &str = + r#"{"error":{"code":"UNKNOWN_OP","integrity":false,"message":"no such operation"}}"#; +const READ_FAILED_JSON: &str = + r#"{"error":{"code":"READ_FAILED","integrity":false,"message":"the host refused a read"}}"#; + +const VERSION_AT: usize = 1024; +const READ_AT: usize = 2048; +const STATUS_AT: usize = 3072; +const UNKNOWN_AT: usize = 4096; +const READ_FAILED_AT: usize = 5120; +/// WebAssembly page size, and the stand-in's initial linear memory. +const PAGE_BYTES: usize = 64 * 1024; +const INITIAL_PAGES: usize = 2; + +const STAND_IN: &str = r#" +(module + (import "env" "host_read" (func $host_read (param i64 i32 i32) (result i32))) + @EXTRA_IMPORT@ + (memory (export "memory") @INITIAL_PAGES@) + (global $heap (mut i32) (i32.const 16384)) + (global $result_ptr (mut i32) (i32.const 0)) + (global $result_len (mut i32) (i32.const 0)) + (data (i32.const @VERSION_AT@) "@VERSION_JSON@") + (data (i32.const @READ_AT@) "@READ_JSON@") + (data (i32.const @STATUS_AT@) "@STATUS_JSON@") + (data (i32.const @UNKNOWN_AT@) "@UNKNOWN_JSON@") + (data (i32.const @READ_FAILED_AT@) "@READ_FAILED_JSON@") + (func (export "probe_abi") (result i32) (i32.const @ABI@)) + (func (export "probe_alloc") (param $length i32) (result i32) + (local $pointer i32) + (local.set $pointer (global.get $heap)) + (global.set $heap (i32.add (global.get $heap) (local.get $length))) + (local.get $pointer)) + (func (export "probe_free") (param i32 i32)) + (func (export "probe_result_ptr") (result i32) (global.get $result_ptr)) + (func (export "probe_result_len") (result i32) (global.get $result_len)) + (func $answer (param $pointer i32) (param $length i32) (param $code i32) (result i32) + (global.set $result_ptr (local.get $pointer)) + (global.set $result_len (local.get $length)) + (local.get $code)) + (func $report (param $value i32) (result i32) + (i32.store8 (i32.const @STATUS_DIGIT@) (i32.add (i32.const 48) (local.get $value))) + (call $answer (i32.const @STATUS_AT@) (i32.const @STATUS_LEN@) (i32.const 0))) + (func $read_failed (result i32) + (call $answer (i32.const @READ_FAILED_AT@) (i32.const @READ_FAILED_LEN@) (i32.const 1))) + (func (export "probe_call") + (param $op i32) (param $op_len i32) (param $arg i32) (param $arg_len i32) (result i32) + (local $kind i32) + (local.set $kind (i32.load8_u (local.get $op))) + ;; "version" + (if (i32.eq (local.get $kind) (i32.const 118)) + (then (return (call $answer (i32.const @VERSION_AT@) (i32.const @VERSION_LEN@) (i32.const 0))))) + ;; "read": bytes 0..4, then bytes 4..8, into the template + (if (i32.eq (local.get $kind) (i32.const 114)) + (then + (if (call $host_read (i64.const 0) (i32.const 4) (i32.const @HEAD_SLOT@)) + (then (return (call $read_failed)))) + (if (call $host_read (i64.const 4) (i32.const 4) (i32.const @NEXT_SLOT@)) + (then (return (call $read_failed)))) + (return (call $answer (i32.const @READ_AT@) (i32.const @READ_LEN@) (i32.const 0))))) + ;; "outside": a read past the end of any test source + (if (i32.eq (local.get $kind) (i32.const 111)) + (then (return (call $report + (call $host_read (i64.const 1000000) (i32.const 4) (i32.const @HEAD_SLOT@)))))) + ;; "memory": a destination past linear memory + (if (i32.eq (local.get $kind) (i32.const 109)) + (then (return (call $report + (call $host_read (i64.const 0) (i32.const 4) (i32.const -16)))))) + ;; "greedy": reads until one is refused + (if (i32.eq (local.get $kind) (i32.const 103)) + (then + (loop $again + (br_if $again (i32.eqz + (call $host_read (i64.const 0) (i32.const 4) (i32.const @HEAD_SLOT@))))) + (return (call $read_failed)))) + ;; "Grow": memory to one page past the bound; reports 0 when refused + (if (i32.eq (local.get $kind) (i32.const 71)) + (then (return (call $report + (i32.add (i32.const 1) (memory.grow (i32.const @OVER_PAGES@))))))) + ;; "allocate": memory up to exactly the bound; reports 0 when refused + (if (i32.eq (local.get $kind) (i32.const 97)) + (then (return (call $report + (i32.add (i32.const 1) (memory.grow (i32.const @FIT_PAGES@))))))) + ;; "Beyond": one byte more than the per-read bound + (if (i32.eq (local.get $kind) (i32.const 66)) + (then (return (call $report + (call $host_read (i64.const 0) (i32.const @READ_BOUND_PLUS_ONE@) (i32.const 0)))))) + ;; "bound": exactly the per-read bound, into more than linear memory + (if (i32.eq (local.get $kind) (i32.const 98)) + (then (return (call $report + (call $host_read (i64.const 0) (i32.const @READ_BOUND@) (i32.const 0)))))) + ;; "flood": per-read-bound reads forever, ignoring every status + (if (i32.eq (local.get $kind) (i32.const 102)) + (then + (loop $again + (drop (call $host_read (i64.const 0) (i32.const @READ_BOUND@) (i32.const 0))) + (br $again)))) + ;; "echo": the argument as received + (if (i32.eq (local.get $kind) (i32.const 101)) + (then (return (call $answer (local.get $arg) (local.get $arg_len) (i32.const 0))))) + ;; "spin": never returns + (if (i32.eq (local.get $kind) (i32.const 115)) + (then (loop $forever (br $forever)))) + ;; "huge": claims a 2 GiB result + (if (i32.eq (local.get $kind) (i32.const 104)) + (then (return (call $answer (i32.const 0) (i32.const 0x7fffffff) (i32.const 0))))) + ;; "x": traps + (if (i32.eq (local.get $kind) (i32.const 120)) + (then (unreachable))) + (call $answer (i32.const @UNKNOWN_AT@) (i32.const @UNKNOWN_LEN@) (i32.const 1))) +) +"#; + +fn planner() -> String { + "a".repeat(64) +} + +fn wat_string(text: &str) -> String { + text.replace('\\', "\\\\").replace('"', "\\\"") +} + +/// The stand-in release, reporting `abi` and `planner`, optionally with a +/// second import no release may have. +fn stand_in(abi: u32, planner: &str, extra_import: bool) -> Vec { + let version = format!(r#"{{"planner_id":"{planner}"}}"#); + let head_slot = READ_AT + READ_JSON.find("____").expect("should hold a head slot"); + let next_slot = READ_AT + READ_JSON.rfind("____").expect("should hold a next slot"); + let status_digit = STATUS_AT + STATUS_JSON.find('0').expect("should hold a digit"); + let extra = if extra_import { + r#"(import "wasi_snapshot_preview1" "fd_write" (func (param i32 i32 i32 i32) (result i32)))"# + } else { + "" + }; + let edge = ProbeLimits::default(); + let fit_pages = edge.memory_bytes / PAGE_BYTES - INITIAL_PAGES; + let read_bound = edge.max_read_bytes; + let text = STAND_IN + .replace("@EXTRA_IMPORT@", extra) + .replace("@ABI@", &abi.to_string()) + .replace("@VERSION_AT@", &VERSION_AT.to_string()) + .replace("@VERSION_JSON@", &wat_string(&version)) + .replace("@VERSION_LEN@", &version.len().to_string()) + .replace("@READ_AT@", &READ_AT.to_string()) + .replace("@READ_JSON@", &wat_string(READ_JSON)) + .replace("@READ_LEN@", &READ_JSON.len().to_string()) + .replace("@HEAD_SLOT@", &head_slot.to_string()) + .replace("@NEXT_SLOT@", &next_slot.to_string()) + .replace("@STATUS_AT@", &STATUS_AT.to_string()) + .replace("@STATUS_JSON@", &wat_string(STATUS_JSON)) + .replace("@STATUS_LEN@", &STATUS_JSON.len().to_string()) + .replace("@STATUS_DIGIT@", &status_digit.to_string()) + .replace("@UNKNOWN_AT@", &UNKNOWN_AT.to_string()) + .replace("@UNKNOWN_JSON@", &wat_string(UNKNOWN_JSON)) + .replace("@UNKNOWN_LEN@", &UNKNOWN_JSON.len().to_string()) + .replace("@READ_FAILED_AT@", &READ_FAILED_AT.to_string()) + .replace("@READ_FAILED_JSON@", &wat_string(READ_FAILED_JSON)) + .replace("@READ_FAILED_LEN@", &READ_FAILED_JSON.len().to_string()) + .replace("@INITIAL_PAGES@", &INITIAL_PAGES.to_string()) + .replace("@FIT_PAGES@", &fit_pages.to_string()) + .replace("@OVER_PAGES@", &(fit_pages + 1).to_string()) + .replace("@READ_BOUND@", &read_bound.to_string()) + .replace("@READ_BOUND_PLUS_ONE@", &(read_bound + 1).to_string()); + wat::parse_str(&text).expect("should assemble the stand-in release") +} + +fn release_key() -> SigningKey { + SigningKey::from_bytes(&[7; 32]) +} + +fn signed( + key: &SigningKey, + abi: u64, + release: u64, + planner: &str, + sha256: &str, +) -> ProbeDescriptor { + let mut descriptor = ProbeDescriptor { + abi, + planner_id: planner.to_owned(), + release, + sha256: sha256.to_owned(), + signature: String::new(), + }; + let signature = key.sign(descriptor.canonical_json().as_bytes()); + descriptor.signature = STANDARD.encode(signature.to_bytes()); + descriptor +} + +fn authority(floor: u64, revoked: Vec) -> ReleaseAuthority { + ReleaseAuthority::pinned(vec![release_key().verifying_key()], floor, revoked) +} + +/// The stand-in, signed as release 3, loaded under `limits`. +fn loaded(limits: ProbeLimits) -> ProbeModule { + let artifact = stand_in(1, &planner(), false); + let descriptor = signed(&release_key(), 1, 3, &planner(), &sha256_hex(&artifact)); + block_on(ProbeModule::load( + &authority(1, Vec::new()), + &descriptor, + &artifact, + limits, + )) + .expect("should load the signed stand-in") +} + +fn load_error(artifact: &[u8], descriptor: &ProbeDescriptor) -> ReleaseError { + block_on(ProbeModule::load( + &authority(1, Vec::new()), + descriptor, + artifact, + ProbeLimits::default(), + )) + .expect_err("should refuse the release") +} + +/// The pinned version changed under the call. +#[derive(Debug, PartialEq, Eq)] +struct VersionChanged; + +impl fmt::Display for VersionChanged { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("the source's ETag changed") + } +} + +/// Returns `Pending` once before completing, as a network read does. +#[derive(Default)] +struct YieldOnce { + yielded: bool, +} + +impl Future for YieldOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> { + if self.yielded { + Poll::Ready(()) + } else { + self.yielded = true; + context.waker().wake_by_ref(); + Poll::Pending + } + } +} + +/// An in-memory source whose every read first suspends. +struct AsyncSource { + bytes: Vec, + size: u64, + fail: bool, + reads: RefCell>, + suspensions: Cell, +} + +impl AsyncSource { + fn new(bytes: &[u8]) -> Self { + Self { + bytes: bytes.to_vec(), + size: bytes.len() as u64, + fail: false, + reads: RefCell::new(Vec::new()), + suspensions: Cell::new(0), + } + } + + fn failing(bytes: &[u8]) -> Self { + Self { + fail: true, + ..Self::new(bytes) + } + } + + /// A source that claims `size` bytes and holds none, for reads the host + /// must refuse before asking it. + fn claiming(size: u64) -> Self { + Self { + size, + ..Self::new(&[]) + } + } + + fn reads(&self) -> Vec<(u64, usize)> { + self.reads.borrow().clone() + } +} + +impl ProbeSource for AsyncSource { + type Error = VersionChanged; + + fn size(&self) -> u64 { + self.size + } + + async fn read_exact_at( + &self, + offset: u64, + destination: &mut [u8], + ) -> Result<(), VersionChanged> { + self.suspensions.set(self.suspensions.get() + 1); + YieldOnce::default().await; + self.reads.borrow_mut().push((offset, destination.len())); + if self.fail { + return Err(VersionChanged); + } + let start = usize::try_from(offset).expect("should fit a test offset"); + destination.copy_from_slice(&self.bytes[start..start + destination.len()]); + Ok(()) + } +} + +/// Release 8 as the production signing service advertised it, signed by +/// Encypher's production release key. Only the public descriptor is kept; +/// its artifact is not part of this repository. +const PRODUCTION_RELEASE_8: &str = r#"{ + "abi": 1, + "planner_id": "0685f74f2b47b171a62be7d8f38224c85b5fdc1fc849d316e8c93910406ece04", + "release": 8, + "sha256": "45c8e965b33e6763a52a9164392fb584f1dd6d993e9583a9f51945caa4ebed59", + "signature": "Mge6x1crHLQtuvn0eDjtapacOVTiEfU7/GOoxRd09e2bFGZaiSn9CvBlf/hSZs6Auvb2aRhWDVeOqnw+iTMMCw==" +}"#; + +#[test] +fn production_authority_accepts_only_releases_encypher_signed() { + let authority = ReleaseAuthority::production(); + let published: serde_json::Value = + serde_json::from_str(PRODUCTION_RELEASE_8).expect("should parse the fixture"); + let published = ProbeDescriptor::parse(&published).expect("should be a descriptor"); + assert_eq!( + authority.select(std::slice::from_ref(&published)), + Ok(&published), + "should accept the release Encypher published" + ); + + let mut retargeted = published.clone(); + retargeted.sha256 = "b".repeat(64); + assert_eq!( + authority.verify(&retargeted), + Err(ReleaseError::Unsigned { release: 8 }), + "should refuse the published signature over another artifact" + ); + let foreign = signed( + &release_key(), + 1, + 8, + &published.planner_id, + &published.sha256, + ); + assert_eq!( + authority.verify(&foreign), + Err(ReleaseError::Unsigned { release: 8 }), + "should refuse the same release signed by any other key" + ); +} + +#[test] +fn descriptors_parse_only_in_the_contract_shape() { + let descriptor = signed(&release_key(), 1, 3, &planner(), &"b".repeat(64)); + assert_eq!( + ProbeDescriptor::parse(&descriptor.to_json()), + Some(descriptor.clone()) + ); + assert_eq!( + descriptor.canonical_json(), + format!( + r#"{{"abi":1,"planner_id":"{}","release":3,"sha256":"{}"}}"#, + planner(), + "b".repeat(64) + ), + "should sign the four fields, sorted, without whitespace" + ); + assert_eq!( + descriptor.artifact_path(), + format!("sign/media/probe/{}.wasm", "b".repeat(64)) + ); + + let mut upper = descriptor.to_json(); + upper["sha256"] = json!("B".repeat(64)); + let mut short = descriptor.to_json(); + short["planner_id"] = json!("a".repeat(63)); + let mut unsigned = descriptor.to_json(); + unsigned["signature"] = json!(""); + let mut textual = descriptor.to_json(); + textual["release"] = json!("3"); + for malformed in [&upper, &short, &unsigned, &textual] { + assert_eq!( + ProbeDescriptor::parse(malformed), + None, + "should refuse {malformed}" + ); + } + let list = json!([descriptor.to_json(), upper, "noise"]); + assert_eq!(ProbeDescriptor::parse_list(Some(&list)), vec![descriptor]); + assert!(ProbeDescriptor::parse_list(Some(&json!({}))).is_empty()); +} + +#[test] +fn verification_refuses_every_tampered_descriptor() { + let authority = authority(2, vec!["c".repeat(64)]); + let good = signed(&release_key(), 1, 3, &planner(), &"b".repeat(64)); + assert_eq!(authority.verify(&good), Ok(())); + + let mut retargeted = good.clone(); + retargeted.sha256 = "d".repeat(64); + assert_eq!( + authority.verify(&retargeted), + Err(ReleaseError::Unsigned { release: 3 }) + ); + + let foreign = signed( + &SigningKey::from_bytes(&[9; 32]), + 1, + 3, + &planner(), + &"b".repeat(64), + ); + assert_eq!( + authority.verify(&foreign), + Err(ReleaseError::Unsigned { release: 3 }) + ); + + for signature in ["not base64!", "AAAA", &STANDARD.encode([0_u8; 63])] { + let mut garbled = good.clone(); + garbled.signature = signature.to_owned(); + assert_eq!( + authority.verify(&garbled), + Err(ReleaseError::MalformedSignature { release: 3 }), + "should refuse signature {signature:?}" + ); + } + + let future_abi = signed(&release_key(), 2, 3, &planner(), &"b".repeat(64)); + assert_eq!( + authority.verify(&future_abi), + Err(ReleaseError::AbiUnsupported { abi: 2 }) + ); + assert_eq!( + authority.verify(&future_abi).map_err(|error| error.code()), + Err("PROBE_UPGRADE_REQUIRED") + ); + + let old = signed(&release_key(), 1, 1, &planner(), &"b".repeat(64)); + assert_eq!( + authority.verify(&old), + Err(ReleaseError::BelowFloor { + release: 1, + floor: 2 + }) + ); + + let withdrawn = signed(&release_key(), 1, 4, &planner(), &"C".repeat(64)); + assert_eq!( + authority.verify(&withdrawn), + Err(ReleaseError::Revoked { release: 4 }) + ); +} + +#[test] +fn selection_takes_the_newest_compatible_release_and_never_falls_back() { + let authority = authority(1, Vec::new()); + let older = signed(&release_key(), 1, 3, &planner(), &"b".repeat(64)); + let newer = signed(&release_key(), 1, 5, &planner(), &"c".repeat(64)); + let future = signed(&release_key(), 2, 9, &planner(), &"d".repeat(64)); + let advertised = [older.clone(), future.clone(), newer.clone()]; + assert_eq!(authority.select(&advertised), Ok(&newer)); + + assert_eq!(authority.select(&[]), Err(ReleaseError::NoneAdvertised)); + assert_eq!( + authority.select(std::slice::from_ref(&future)), + Err(ReleaseError::NoCompatibleRelease) + ); + + let mut tampered = newer; + tampered.planner_id = "e".repeat(64); + assert_eq!( + authority.select(&[older, tampered]), + Err(ReleaseError::Unsigned { release: 5 }), + "should refuse the newest rather than run an older release" + ); +} + +#[test] +fn loading_refuses_artifacts_that_are_not_the_signed_release() { + let artifact = stand_in(1, &planner(), false); + let descriptor = signed(&release_key(), 1, 3, &planner(), &sha256_hex(&artifact)); + + let mut tampered = artifact.clone(); + let last = tampered.len() - 1; + tampered[last] ^= 1; + assert_eq!( + load_error(&tampered, &descriptor), + ReleaseError::DigestMismatch + ); + + let oversized = vec![0_u8; MAX_ARTIFACT_BYTES + 1]; + let oversized_descriptor = signed(&release_key(), 1, 3, &planner(), &sha256_hex(&oversized)); + assert_eq!( + load_error(&oversized, &oversized_descriptor), + ReleaseError::ArtifactTooLarge { + bytes: MAX_ARTIFACT_BYTES + 1 + } + ); + + let garbage = b"not a module".to_vec(); + let garbage_descriptor = signed(&release_key(), 1, 3, &planner(), &sha256_hex(&garbage)); + assert!(matches!( + load_error(&garbage, &garbage_descriptor), + ReleaseError::InvalidModule(_) + )); + let mut unsigned = garbage_descriptor; + unsigned.release = 4; + assert_eq!( + load_error(&garbage, &unsigned), + ReleaseError::Unsigned { release: 4 }, + "should refuse the descriptor before parsing its artifact" + ); +} + +#[test] +fn loading_refuses_a_signed_release_that_breaks_the_abi() { + let importing = stand_in(1, &planner(), true); + let descriptor = signed(&release_key(), 1, 3, &planner(), &sha256_hex(&importing)); + assert_eq!( + load_error(&importing, &descriptor), + ReleaseError::ImportsMismatch + ); + + let misreporting = stand_in(2, &planner(), false); + let descriptor = signed(&release_key(), 1, 3, &planner(), &sha256_hex(&misreporting)); + assert_eq!( + load_error(&misreporting, &descriptor), + ReleaseError::AbiReported { reported: 2 } + ); + + let other_planner = stand_in(1, &"f".repeat(64), false); + let descriptor = signed( + &release_key(), + 1, + 3, + &planner(), + &sha256_hex(&other_planner), + ); + assert_eq!( + load_error(&other_planner, &descriptor), + ReleaseError::PlannerMismatch + ); +} + +#[test] +fn reads_are_awaited_in_order_and_the_call_resumes_without_replay() { + let probe = loaded(ProbeLimits::default()); + assert_eq!(probe.descriptor().release, 3); + let source = AsyncSource::new(b"abcdefgh-rest-of-a-large-file"); + + let value = block_on(probe.call("read", &json!({}), &source)).expect("should read"); + + assert_eq!(value, json!({"head": "abcd", "next": "efgh"})); + assert_eq!( + source.reads(), + vec![(0, 4), (4, 4)], + "each read should reach the source once, in order" + ); + assert_eq!( + source.suspensions.get(), + 2, + "each read should suspend the call" + ); + + let again = block_on(probe.call("read", &json!({}), &source)).expect("should read again"); + assert_eq!( + again, value, + "a second call should start from a fresh instance" + ); + assert_eq!(source.reads().len(), 4); +} + +#[test] +fn the_argument_reaches_the_release_intact() { + let probe = loaded(ProbeLimits::default()); + let arg = json!({"plan": {"items": [1, 2, 3]}, "text": "caf\u{e9} \"quoted\""}); + + let echoed = + block_on(probe.call_without_source::("echo", &arg)).expect("should echo"); + + assert_eq!(echoed, arg); +} + +#[test] +fn reads_outside_the_source_or_memory_are_refused_without_asking_the_source() { + let probe = loaded(ProbeLimits::default()); + let source = AsyncSource::new(b"abcdefgh"); + + let outside = block_on(probe.call("outside", &json!({}), &source)).expect("should answer"); + assert_eq!(outside, json!({"status": 5})); + + let memory = block_on(probe.call("memory", &json!({}), &source)).expect("should answer"); + assert_eq!(memory, json!({"status": 4})); + + assert!( + source.reads().is_empty(), + "no refused read should reach the source" + ); +} + +#[test] +fn the_edge_per_read_bound_admits_its_size_and_refuses_one_byte_more() { + let probe = loaded(ProbeLimits::default()); + let bound = u64::from(ProbeLimits::default().max_read_bytes); + let source = AsyncSource::claiming(bound + 1); + + let beyond = block_on(probe.call("Beyond", &json!({}), &source)).expect("should answer"); + assert_eq!( + beyond, + json!({"status": 5}), + "a read over the bound should be refused" + ); + + let at_bound = block_on(probe.call("bound", &json!({}), &source)).expect("should answer"); + assert_eq!( + at_bound, + json!({"status": 4}), + "a read at the bound should pass the bound and stop only at linear memory" + ); + assert!( + source.reads().is_empty(), + "neither read should reach the source" + ); +} + +#[test] +fn the_edge_total_read_budget_stops_a_release_that_keeps_reading() { + let probe = loaded(ProbeLimits::default()); + let source = AsyncSource::claiming(u64::from(ProbeLimits::default().max_read_bytes)); + + let error = block_on(probe.call("flood", &json!({}), &source)).expect_err("should stop"); + + assert!( + matches!( + error, + ProbeCallError::Failed(ProbeFailure::ReadBudgetExceeded { .. }) + ), + "the read budget, not fuel, should stop the release: {error}" + ); +} + +#[test] +fn the_read_budget_counts_every_admitted_read() { + let probe = loaded(ProbeLimits { + max_total_read_bytes: 8, + ..ProbeLimits::default() + }); + let source = AsyncSource::new(b"abcdefgh"); + + let error = block_on(probe.call("greedy", &json!({}), &source)).expect_err("should stop"); + + assert!(matches!( + error, + ProbeCallError::Failed(ProbeFailure::ReadBudgetExceeded { budget: 8 }) + )); + assert_eq!(source.reads(), vec![(0, 4), (0, 4)]); +} + +#[test] +fn a_failed_read_abandons_the_call_with_the_source_error() { + let probe = loaded(ProbeLimits::default()); + let source = AsyncSource::failing(b"abcdefgh"); + + let error = block_on(probe.call("read", &json!({}), &source)).expect_err("should fail"); + + assert!(matches!(error, ProbeCallError::Source(VersionChanged))); + assert_eq!( + source.reads(), + vec![(0, 4)], + "no read should follow the failure" + ); +} + +#[test] +fn edge_fuel_stops_a_release_that_never_returns() { + let probe = loaded(ProbeLimits::default()); + + let spin = block_on(probe.call_without_source::("spin", &json!({}))); + + assert!(matches!( + spin, + Err(ProbeCallError::Failed(ProbeFailure::FuelExhausted)) + )); +} + +#[test] +fn edge_memory_grows_to_its_bound_and_no_further() { + let probe = loaded(ProbeLimits::default()); + + let to_bound = block_on(probe.call_without_source::("allocate", &json!({}))) + .expect("should answer"); + assert_eq!( + to_bound, + json!({"status": 3}), + "growth to exactly the bound should succeed" + ); + + let past_bound = block_on(probe.call_without_source::("Grow", &json!({}))) + .expect("should answer"); + assert_eq!( + past_bound, + json!({"status": 0}), + "growth one page past the bound should fail inside the release" + ); +} + +#[test] +fn an_oversized_result_is_refused() { + let probe = loaded(ProbeLimits::default()); + + let huge = block_on(probe.call_without_source::("huge", &json!({}))); + + assert!(matches!( + huge, + Err(ProbeCallError::Failed(ProbeFailure::ResultTooLarge { .. })) + )); +} + +#[test] +fn traps_and_error_objects_are_reported_as_such() { + let probe = loaded(ProbeLimits::default()); + + let trap = block_on(probe.call_without_source::("x", &json!({}))); + assert!(matches!( + trap, + Err(ProbeCallError::Failed(ProbeFailure::Trap(_))) + )); + + let unknown = block_on(probe.call_without_source::("unknown", &json!({}))) + .expect_err("should refuse"); + assert_eq!(unknown.code(), "UNKNOWN_OP"); + let ProbeCallError::Refused(refusal) = unknown else { + panic!("should carry the release's error object"); + }; + assert!(!refusal.integrity); + assert_eq!(refusal.message, "no such operation"); +} diff --git a/crates/provenance/encypher/src/processor.rs b/crates/provenance/encypher/src/processor.rs new file mode 100644 index 0000000000..6afff27cc9 --- /dev/null +++ b/crates/provenance/encypher/src/processor.rs @@ -0,0 +1,3731 @@ +//! Per-document signing flow: gates, hold, KV cache, sign calls, outcomes, logging. +//! +//! A document moves through four phases: +//! +//! 1. **Admit** (the middleware's preparation, with no I/O): the request +//! gates decide whether the document is held at all. +//! 2. **Extract** (synchronous): at end of input the held document's article +//! boundary, visible text and text hash are computed, and its embedded +//! media found. +//! 3. **Resolve** (the asynchronous finish): policy and plan caches, the policy +//! and sign calls, and cache writes, then each embedded media item signed +//! through [`media`]. On runtimes with a timer every call is raced against +//! one `max_hold_ms` budget, keeping a reserve for the skip write. +//! 4. **Render** (synchronous): the plan's markers and the manifest meta tag are +//! applied to the held document, then each signed media item replaces its +//! exact bytes. +//! +//! Every module failure emits the held document unchanged, or, for one +//! media item, leaves that item as written and reports it unsigned. + +use std::borrow::Cow; +use std::io; +use std::pin::pin; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use bytes::Bytes; +use edgezero_core::body::Body as EdgeBody; +use futures::future::{Either, LocalBoxFuture, select}; +use http::{HeaderValue, Method, Request, StatusCode, header}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +use crate::api::{self, ApiError, EmbeddingPlan, SignRequest}; +use crate::boundary::{self, Boundary}; +use crate::embedded; +use crate::{EncypherConfig, INTEGRATION_ID, embed, text}; +use trusted_server_core::integrations::ensure_integration_backend_with_timeout; +use trusted_server_core::platform::{ + KvError, PlatformHttpRequest, PlatformKvStore, PlatformTimer, RuntimeServices, +}; +use trusted_server_core::streaming_processor::{AsyncFinish, StreamProcessor}; + +mod media; +mod prepared; +mod published; + +pub(crate) use media::EncypherMedia; + +const SIGN_PATH: &str = "/api/v1/public/cdn/sign"; +const POLICY_PATH: &str = "/api/v1/public/cdn/trusted-server/policy"; +const WORKER_TOKEN_HEADER: &str = "x-encypher-worker-token"; +/// Successful article plans. Entries carry their own `expires_at`. +const PLAN_KEY_PREFIX: &str = "encypher:plan:v4"; +/// Article failures, separate so a delayed failure cannot replace a plan. +const PLAN_SKIP_KEY_PREFIX: &str = "encypher:plan-skip:v4"; +/// Successful policy digests. Entries carry their own `expires_at`. +const POLICY_KEY_PREFIX: &str = "encypher:policy:v3"; +/// Policy failures, separate so a delayed failure cannot replace a digest. +const POLICY_SKIP_KEY_PREFIX: &str = "encypher:policy-skip:v3"; +/// Part of `max_hold_ms` kept back from resolve work so a skip can still be +/// written after the work runs out of time. +const SKIP_WRITE_RESERVE: Duration = Duration::from_millis(200); +/// How far past `max_hold_ms` the core backstop deadline lies. The module +/// bounds itself first; the backstop only catches an overrun. +const BACKSTOP_MARGIN: Duration = Duration::from_millis(1_000); + +/// Path prefixes never signed (ported from the Fastly worker's `shouldSkipPath`). +const SKIP_PATH_PREFIXES: &[&str] = &[ + "/api/", + "/wp-admin/", + "/wp-json/", + "/feed/", + "/sitemap", + "/.well-known/", +]; +/// Asset suffixes the article path never signs (ported from the Fastly +/// worker's `shouldSkipPath`). Media at these paths is signed by [`media`]. +const SKIP_PATH_SUFFIXES: &[&str] = &[ + ".xml", ".json", ".rss", ".atom", ".css", ".js", ".woff", ".woff2", ".png", ".jpg", ".gif", + ".svg", ".ico", +]; + +/// Set once this instance has warned that the runtime KV store is unbound. +static KV_UNAVAILABLE_WARNED: AtomicBool = AtomicBool::new(false); + +/// Why a document was not held. Evaluated in the middleware's preparation, +/// with no I/O. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Gate { + Method, + Status, + OriginPrivate, + InvalidPageUrl, + HostMismatch, + ExcludedPath, +} + +impl Gate { + pub(crate) fn code(self) -> &'static str { + match self { + Self::Method => "method", + Self::Status => "status", + Self::OriginPrivate => "origin_private", + Self::InvalidPageUrl => "invalid_page_url", + Self::HostMismatch => "host_mismatch", + Self::ExcludedPath => "excluded_path", + } + } +} + +/// What the gates are told about the page a document is the body of. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PageRequest { + /// The reader's request method. + pub(crate) method: Method, + /// The scheme, host and path the reader asked for, without the query, + /// which is the address the article is signed for. On a host that + /// serves several publishers by path it includes the publisher's prefix. + pub(crate) page_url: String, + /// The reader's query, without its `?`, which only the asynchronous + /// mode reads, to admit a configured variant. + pub(crate) query: String, + /// The path prefix request addressing removed, such as + /// `/www.example.com`, or empty. The path gates read the publisher's own + /// path, without it. + pub(crate) removed_prefix: String, + /// The status of the response the document is the body of. + pub(crate) response_status: u16, + /// Whether the origin marked its response as one reader's. + pub(crate) origin_response_is_private: bool, +} + +/// Reason code for a held document's or a media item's outcome. Sign +/// failures are also stored as the reason of a negative (`skip`) cache entry. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum Reason { + Signed, + CacheHit, + DocumentTooLarge, + OutputTooLarge, + NotUtf8, + NoBoundary, + AlreadySigned, + ManifestPresent, + TextTooShort, + KvUnavailable, + KvError, + Timeout, + CachedSkip, + CachedEntryInvalid, + CachedPlanInvalid, + RequestBuild, + Backend, + HttpError, + Forbidden, + Unprocessable, + RateLimited, + ServerError, + UnexpectedStatus, + ResponseTooLarge, + MalformedResponse, + PolicyChanged, + PolicyDigestMismatch, + QuotaExceeded, + SignRejected, + NoPlan, + InvalidPlan, + AlignmentFailed, + /// The media is over `max_media_bytes`. + MediaTooLarge, + /// The media is delivered outside the claimed domain. + OutsideClaim, + /// Core named no single hostname the source was fetched from. + InvalidOriginHost, + /// The captured bytes are not the bytes core hashed. + SourceDigestMismatch, + /// A `data:` URI's payload does not decode. + MalformedDataUri, + /// The endpoint answered `409` to a reused replay key. + IdempotencyConflict, + /// The endpoint no longer holds the result for the replay key. + ResultPurged, + /// The endpoint refused the request body's size. + PayloadTooLarge, + /// The signed bytes are of another media type than the source. + SignedTypeMismatch, + /// The signed bytes are not the bytes the answer names. + SignedDigestMismatch, + /// A signing header is missing, repeated or malformed. + InvalidSignedResponse, + /// The manifest is remote and needs a `Link` the delivery cannot carry. + ManifestUndeliverable, + /// The answer carries no manifest and no reference to one. + UnsignedResponse, + /// The response's policies are not visible to the module, so an inline + /// style edit could break a style hash. + CspUnknown, + /// A page policy admits the inline style by hash. + CspStyleHash, + /// The element carries a subresource integrity value. + IntegrityAttribute, + /// The URI is written with an HTML character reference. + CharacterReference, + /// The URI is written with a CSS escape. + CssEscape, + /// No standalone SVG document is proven to parse to the tree the browser + /// built from the inline SVG. + InlineSvgNotEquivalent, + /// The inline SVG carries provenance of its own and is not, as written, + /// a standalone SVG equivalent to the browser's tree, so it is never + /// rewritten. + SvgProvenanceNotEquivalent, + /// Original SVG provenance protects the entire range from inline edits + /// or replacement with a new, unchained signature. + SvgExistingProvenance, + /// The signer changed the inline SVG beyond adding its manifest. + InlineSvgAltered, + /// The document's media made its most endpoint calls; the item is signed + /// on a later view. + Deferred, + /// No published result exists for the item's exact identity: it is + /// delivered unsigned and the worker may be hinted. + NotPrepared, + /// The identity's result was retired; it is never served again. + Retired, + /// The retirement marker could not be read, so no result is served. + MarkerUnavailable, + /// A published record does not match the identity or item it is for. + PublishedRecordInvalid, + /// The optimizer's output is never looked up in the asynchronous mode, + /// whose standalone results are external manifests over origin bytes. + OptimizerOutputUnsupported, +} + +impl Reason { + pub(crate) fn code(self) -> &'static str { + match self { + Self::Signed => "signed", + Self::CacheHit => "cache_hit", + Self::DocumentTooLarge => "document_too_large", + Self::OutputTooLarge => "output_too_large", + Self::NotUtf8 => "not_utf8", + Self::NoBoundary => "no_boundary", + Self::AlreadySigned => "already_signed", + Self::ManifestPresent => "manifest_present", + Self::TextTooShort => "text_too_short", + Self::KvUnavailable => "kv_unavailable", + Self::KvError => "kv_error", + Self::Timeout => "timeout", + Self::CachedSkip => "cached_skip", + Self::CachedEntryInvalid => "cached_entry_invalid", + Self::CachedPlanInvalid => "cached_plan_invalid", + Self::RequestBuild => "request_build", + Self::Backend => "backend", + Self::HttpError => "http_error", + Self::Forbidden => "forbidden", + Self::Unprocessable => "unprocessable", + Self::RateLimited => "rate_limited", + Self::ServerError => "server_error", + Self::UnexpectedStatus => "unexpected_status", + Self::ResponseTooLarge => "response_too_large", + Self::MalformedResponse => "malformed_response", + Self::PolicyChanged => "policy_changed", + Self::PolicyDigestMismatch => "policy_digest_mismatch", + Self::QuotaExceeded => "quota_exceeded", + Self::SignRejected => "sign_rejected", + Self::NoPlan => "no_plan", + Self::InvalidPlan => "invalid_plan", + Self::AlignmentFailed => "alignment_failed", + Self::MediaTooLarge => "media_too_large", + Self::OutsideClaim => "outside_claim", + Self::InvalidOriginHost => "invalid_origin_host", + Self::SourceDigestMismatch => "source_digest_mismatch", + Self::MalformedDataUri => "malformed_data_uri", + Self::IdempotencyConflict => "idempotency_conflict", + Self::ResultPurged => "result_purged", + Self::PayloadTooLarge => "payload_too_large", + Self::SignedTypeMismatch => "signed_type_mismatch", + Self::SignedDigestMismatch => "signed_digest_mismatch", + Self::InvalidSignedResponse => "invalid_signed_response", + Self::ManifestUndeliverable => "manifest_undeliverable", + Self::UnsignedResponse => "unsigned_response", + Self::CspUnknown => "csp_unknown", + Self::CspStyleHash => "csp_style_hash", + Self::IntegrityAttribute => "integrity_attribute", + Self::CharacterReference => "character_reference", + Self::CssEscape => "css_escape", + Self::InlineSvgNotEquivalent => "inline_svg_not_equivalent", + Self::SvgProvenanceNotEquivalent => "svg_provenance_not_equivalent", + Self::SvgExistingProvenance => "svg_existing_provenance", + Self::InlineSvgAltered => "inline_svg_altered", + Self::Deferred => "deferred", + Self::NotPrepared => "not_prepared", + Self::Retired => "retired", + Self::MarkerUnavailable => "marker_unavailable", + Self::PublishedRecordInvalid => "published_record_invalid", + Self::OptimizerOutputUnsupported => "optimizer_output_unsupported", + } + } +} + +/// A cache entry that records when it stops being usable. +trait Expiring { + /// Unix seconds at which the entry expires. + fn expires_at(&self) -> u64; + /// Whether the entry was written without a store TTL, so it stays until deleted. + fn ttl_less(&self) -> bool; + /// Record that the entry is written without a store TTL. + fn mark_ttl_less(&mut self); +} + +/// An article cache entry, keyed by credential context, policy and text hash. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +enum CacheEntry { + Plan { + plan: EmbeddingPlan, + manifest_url: Option, + expires_at: u64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + ttl_less: bool, + }, + Skip { + reason: Reason, + expires_at: u64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + ttl_less: bool, + }, +} + +impl Expiring for CacheEntry { + fn expires_at(&self) -> u64 { + match self { + Self::Plan { expires_at, .. } | Self::Skip { expires_at, .. } => *expires_at, + } + } + + fn ttl_less(&self) -> bool { + match self { + Self::Plan { ttl_less, .. } | Self::Skip { ttl_less, .. } => *ttl_less, + } + } + + fn mark_ttl_less(&mut self) { + match self { + Self::Plan { ttl_less, .. } | Self::Skip { ttl_less, .. } => *ttl_less = true, + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +enum PolicyEntry { + Digest { + policy_digest: String, + expires_at: u64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + ttl_less: bool, + }, + Skip { + reason: Reason, + expires_at: u64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + ttl_less: bool, + }, +} + +impl Expiring for PolicyEntry { + fn expires_at(&self) -> u64 { + match self { + Self::Digest { expires_at, .. } | Self::Skip { expires_at, .. } => *expires_at, + } + } + + fn ttl_less(&self) -> bool { + match self { + Self::Digest { ttl_less, .. } | Self::Skip { ttl_less, .. } => *ttl_less, + } + } + + fn mark_ttl_less(&mut self) { + match self { + Self::Digest { ttl_less, .. } | Self::Skip { ttl_less, .. } => *ttl_less = true, + } + } +} + +/// The one freshness predicate applied to every cache read. +fn is_fresh(expires_at: u64, now: u64) -> bool { + now < expires_at +} + +/// Current Unix time in seconds, from a clock that works on every adapter. +fn unix_now() -> u64 { + web_time::SystemTime::now() + .duration_since(web_time::UNIX_EPOCH) + .map_or(0, |elapsed| elapsed.as_secs()) +} + +/// Configuration shared by every document processor. +pub(crate) struct Signer { + config: EncypherConfig, + max_document_bytes: usize, + sign_url: String, + media_sign_url: String, + policy_url: String, + policy_key: String, + policy_skip_key: String, + /// First 16 hex characters of `sha256(api_base_url \n domain \n token)`. + cache_context: String, + /// Set by the first TTL write the store reports as unsupported, or by reading + /// an entry written without a TTL; from then on this instance writes without + /// a TTL and deletes expired entries it reads. + ttl_less: AtomicBool, +} + +impl Signer { + /// The signer for `config`, holding at most `max_document_bytes` of one + /// document. + pub(crate) fn new(config: EncypherConfig, max_document_bytes: usize) -> Self { + let sign_url = format!("{}{SIGN_PATH}", config.api_base_url.trim_end_matches('/')); + let media_sign_url = format!( + "{}{}", + config.api_base_url.trim_end_matches('/'), + crate::media_api::MEDIA_SIGN_PATH + ); + let policy_url = format!("{}{POLICY_PATH}", config.api_base_url.trim_end_matches('/')); + // One credential context for every path, the preparation worker's + // identities included. + let cache_context = crate::preparation::protocol::credential_context( + &config.api_base_url, + &config.domain, + config.worker_token.expose(), + ); + Self { + config, + max_document_bytes, + sign_url, + media_sign_url, + policy_url, + policy_key: format!("{POLICY_KEY_PREFIX}:{cache_context}"), + policy_skip_key: format!("{POLICY_SKIP_KEY_PREFIX}:{cache_context}"), + cache_context, + ttl_less: AtomicBool::new(false), + } + } +} + +/// What a held document is, which decides what in it is signed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Surface { + /// An HTML page: its article's text, then its embedded media. + Page, + /// A stylesheet: the media its `url()`s embed. + Stylesheet, +} + +/// Holds the document of an admitted page until it ends. `policies` are the +/// response's `Content-Security-Policy` values, `None` when core cannot +/// show them, which decide whether inline style media may be edited. +pub(crate) fn document_processor( + signer: &Arc, + request: PageRequest, + policies: Option>, +) -> DocumentProcessor { + held_processor(signer, request, Surface::Page, policies) +} + +/// Holds an admitted stylesheet until it ends. +pub(crate) fn stylesheet_processor( + signer: &Arc, + request: PageRequest, +) -> DocumentProcessor { + held_processor(signer, request, Surface::Stylesheet, None) +} + +fn held_processor( + signer: &Arc, + request: PageRequest, + surface: Surface, + policies: Option>, +) -> DocumentProcessor { + DocumentProcessor { + signer: Arc::clone(signer), + request, + surface, + policies, + held: Vec::new(), + state: DocumentState::Holding, + } +} + +/// What a held document's signing works for. +struct Target<'a> { + request: &'a PageRequest, + surface: Surface, + /// The response's policy values, `None` when core cannot show them. + policies: Option<&'a [String]>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DocumentState { + /// Accumulating the document. + Holding, + /// Over the size limit: chunks pass through and nothing is signed. + PassingThrough, + /// The whole document was emitted as held output; it may be signed in the + /// asynchronous finish. + Complete, +} + +/// Holds one admitted document until end of input, then signs it in the +/// asynchronous finish or passes it through. +pub(crate) struct DocumentProcessor { + signer: Arc, + request: PageRequest, + surface: Surface, + policies: Option>, + held: Vec, + state: DocumentState, +} + +impl StreamProcessor for DocumentProcessor { + fn process_chunk(&mut self, chunk: &[u8], is_last: bool) -> Result, io::Error> { + if self.state != DocumentState::Holding { + return Ok(chunk.to_vec()); + } + if self.held.len().saturating_add(chunk.len()) > self.signer.max_document_bytes { + self.state = DocumentState::PassingThrough; + log_outcome(false, Reason::DocumentTooLarge); + let mut output = std::mem::take(&mut self.held); + output.extend_from_slice(chunk); + return Ok(output); + } + self.held.extend_from_slice(chunk); + if !is_last { + return Ok(Vec::new()); + } + // The complete document is the fail-open output; the driver owns it. + self.state = DocumentState::Complete; + Ok(std::mem::take(&mut self.held)) + } + + fn reset(&mut self) { + self.held.clear(); + self.state = DocumentState::Holding; + } + + fn async_finish<'a>( + &'a mut self, + held: &'a [u8], + services: &'a RuntimeServices, + ) -> Option> { + if self.state != DocumentState::Complete { + return None; + } + let Ok(document) = std::str::from_utf8(held) else { + log_outcome(false, Reason::NotUtf8); + return None; + }; + let (article, media) = match self.surface { + Surface::Page => ( + Some(self.signer.extract(document)), + embedded::scan_html(document, self.policies.as_deref()), + ), + Surface::Stylesheet => (None, embedded::scan_stylesheet(document)), + }; + let article = match article { + Some(Ok(article)) => Some(article), + Some(Err(reason)) => { + log_outcome(false, reason); + None + } + None => None, + }; + if article.is_none() && media.is_empty() { + return None; + } + let signer = &*self.signer; + let target = Target { + request: &self.request, + surface: self.surface, + policies: self.policies.as_deref(), + }; + let deadline = + Duration::from_millis(signer.config.max_hold_ms).saturating_add(BACKSTOP_MARGIN); + Some(AsyncFinish::new(deadline, async move { + signer + .resolve_document(&target, services, document, article, media) + .await + })) + } +} + +/// The article found in a held document. +pub(crate) struct Article { + pub(crate) boundary: Boundary, + /// Visible text sent for signing, truncated to the API limit. + pub(crate) text: String, + /// Unicode scalars in `text`. + pub(crate) scalars: usize, + /// First structural closing-head offset in the original document. + pub(crate) head_close: Option, + /// First completed structural title's inner byte range. + pub(crate) title_range: Option<(usize, usize)>, +} + +/// Find the article and its signable text, with no I/O. The one +/// implementation every path uses: the reader's, synchronous or +/// asynchronous, and the preparation worker's. +pub(crate) fn extract_article( + html: &str, + article_selector: &str, + min_text_length: usize, +) -> Result { + let boundary = + boundary::find_article_boundary(html, Some(article_selector)).ok_or(Reason::NoBoundary)?; + let article = html + .get(boundary.start..boundary.end) + .ok_or(Reason::NoBoundary)?; + if embed::has_existing_markers(article) { + return Err(Reason::AlreadySigned); + } + let metadata = text::document_metadata(html); + if metadata.manifest_present { + return Err(Reason::ManifestPresent); + } + let (visible, scalars) = text::visible_text(article, api::MAX_TEXT_SCALARS); + if scalars < min_text_length { + return Err(Reason::TextTooShort); + } + Ok(Article { + boundary, + text: visible, + scalars, + head_close: metadata.head_close, + title_range: metadata.title_range, + }) +} + +/// Time a document's resolve may still take, on runtimes with a timer. +enum Deadline<'a> { + /// No timer: calls are bounded only by backend timeouts. + Unbounded, + /// The timer has not fired yet. + Running(LocalBoxFuture<'a, ()>), + /// The timer fired; every further race loses at once. + Expired, +} + +impl<'a> Deadline<'a> { + fn new(timer: Option<&'a dyn PlatformTimer>, budget: Duration) -> Self { + timer.map_or(Self::Unbounded, |timer| Self::Running(timer.sleep(budget))) + } + + /// Start the clock now. A timer's sleep starts on its first poll, which + /// would otherwise be the first call that does not complete at once. + async fn start(&mut self) { + if let Self::Running(sleep) = self + && futures::poll!(sleep.as_mut()).is_ready() + { + *self = Self::Expired; + } + } + + /// Run `work` to completion, or return `None` once the deadline passes. + async fn race(&mut self, work: impl Future) -> Option { + match self { + Self::Unbounded => return Some(work.await), + Self::Expired => return None, + Self::Running(sleep) => { + if let Either::Left((value, _)) = select(pin!(work), sleep.as_mut()).await { + return Some(value); + } + } + } + *self = Self::Expired; + None + } + + /// Whether the deadline has passed, so the last race lost. + fn expired(&self) -> bool { + matches!(self, Self::Expired) + } +} + +fn log_outcome(signed: bool, reason: Reason) { + if reason == Reason::KvUnavailable && !KV_UNAVAILABLE_WARNED.swap(true, Ordering::Relaxed) { + log::warn!( + "encypher: the runtime KV store is unavailable, so pages pass through unsigned; bind the adapter's KV store to enable signing" + ); + } + log::info!( + "encypher: outcome={} reason={}", + if signed { "signed" } else { "passed_through" }, + reason.code() + ); +} + +/// Whether `path` is one the article path never signs: an API, feed, admin +/// or well-known path, or an asset. +pub(crate) fn is_excluded_path(path: &str) -> bool { + let lower = path.to_ascii_lowercase(); + is_excluded_prefix(&lower) + || SKIP_PATH_SUFFIXES + .iter() + .any(|suffix| lower.ends_with(suffix)) +} + +/// Whether `path` is an API, feed, admin or well-known path, where no media +/// is signed either. Asset suffixes are not excluded: media is. +pub(crate) fn is_excluded_prefix(path: &str) -> bool { + let lower = path.to_ascii_lowercase(); + SKIP_PATH_PREFIXES + .iter() + .any(|prefix| lower.starts_with(prefix)) +} + +impl Signer { + /// Apply the request gates to a page. Only an admitted document is held. + pub(crate) fn admit(&self, request: PageRequest) -> Result { + self.gate(request, true) + } + + /// Apply the request gates to a stylesheet, whose `.css` path the page + /// gates exclude. + pub(crate) fn admit_stylesheet(&self, request: PageRequest) -> Result { + self.gate(request, false) + } + + fn gate(&self, request: PageRequest, page: bool) -> Result { + if request.method != Method::GET { + return Err(Gate::Method); + } + if request.response_status != StatusCode::OK.as_u16() { + return Err(Gate::Status); + } + if request.origin_response_is_private { + return Err(Gate::OriginPrivate); + } + let page_url = url::Url::parse(&request.page_url).map_err(|_| Gate::InvalidPageUrl)?; + let host = page_url.host_str().ok_or(Gate::InvalidPageUrl)?; + if !self.claims_host(host) { + return Err(Gate::HostMismatch); + } + // The publisher's own path, as its site names it. + let path = page_url.path(); + let path = path + .strip_prefix(request.removed_prefix.as_str()) + .filter(|rest| rest.starts_with('/')) + .unwrap_or(path); + let excluded = if page { + is_excluded_path(path) + } else { + is_excluded_prefix(path) + }; + if excluded { + return Err(Gate::ExcludedPath); + } + Ok(request) + } + + /// Whether `host` is the claimed domain or its `www.` name. + pub(crate) fn claims_host(&self, host: &str) -> bool { + host.strip_prefix("www.").unwrap_or(host) == self.config.domain + } + + /// Find the article and its signable text, with no I/O. + fn extract(&self, html: &str) -> Result { + extract_article( + html, + &self.config.article_selector, + self.config.min_text_length, + ) + } + + /// Unix time at which an entry written now with `ttl_seconds` expires. + fn expires_at(ttl_seconds: u64) -> u64 { + unix_now().saturating_add(ttl_seconds) + } + + /// Bound for skip writes made after resolve work stopped. + fn skip_deadline<'a>(services: &'a RuntimeServices) -> Deadline<'a> { + Deadline::new(services.timer(), SKIP_WRITE_RESERVE) + } + + /// Sign what a held document carries: the article's text first, as the + /// page path always has, then each embedded media item, within one + /// `max_hold_ms` budget. Returns the bytes to emit. + async fn resolve_document( + &self, + target: &Target<'_>, + services: &RuntimeServices, + document: &str, + article: Option
, + media: embedded::Scan, + ) -> Vec { + let request = target.request; + let mut work = Deadline::new( + services.timer(), + Duration::from_millis(self.config.max_hold_ms).saturating_sub(SKIP_WRITE_RESERVE), + ); + work.start().await; + // The asynchronous mode only looks up what the delivery instance + // published; it never signs here, and never falls back to signing. + if self.is_async() { + return self + .resolve_published_document(target, services, document, article, media, &mut work) + .await; + } + let policy_digest = match self + .policy_digest(&request.page_url, services, services.kv_store(), &mut work) + .await + { + Ok(policy_digest) => policy_digest, + Err(reason) => { + if article.is_some() { + log_outcome(false, reason); + } + media::log_unsigned(&media, reason); + return document.as_bytes().to_vec(); + } + }; + let mut output = Cow::Borrowed(document); + if let Some(article) = &article { + match self + .resolve( + request, + services, + document, + article, + &policy_digest, + &mut work, + ) + .await + { + Ok((signed, reason)) => { + log_outcome(true, reason); + output = Cow::Owned(signed); + } + Err(reason) => log_outcome(false, reason), + } + } + if media.is_empty() { + return output.into_owned().into_bytes(); + } + // Markers go into text and the manifest meta tag into the head, never + // into an attribute, a style or an SVG, so a rescan finds the same + // media at its new offsets. + let media = match (&output, target.surface) { + (Cow::Owned(signed), Surface::Page) => embedded::scan_html(signed, target.policies), + _ => media, + }; + // The text plan may come from the cached policy, but media is signed + // or reused only under the policy the service answers now, so a + // revoked credential cannot reuse a cached media result. + let media_policy = match self + .request_policy(&request.page_url, services, &mut work) + .await + { + Ok(policy_digest) => policy_digest, + Err(reason) => { + media::log_unsigned(&media, reason); + return output.into_owned().into_bytes(); + } + }; + let embedded = media::EmbeddedJob { + request, + surface: target.surface, + policy_digest: &media_policy, + }; + match self + .sign_embedded(services, &embedded, &output, &media, &mut work) + .await + { + Some(signed) => signed.into_bytes(), + None => output.into_owned().into_bytes(), + } + } + + /// Resolve the plan for `article` under `policy_digest` and render it + /// into `html`. + async fn resolve( + &self, + request: &PageRequest, + services: &RuntimeServices, + html: &str, + article: &Article, + policy_digest: &str, + work: &mut Deadline<'_>, + ) -> Result<(String, Reason), Reason> { + let kv = services.kv_store(); + let suffix = format!( + "{}:{policy_digest}:{}", + self.cache_context, + text::content_hash_hex(&article.text) + ); + let plan_key = format!("{PLAN_KEY_PREFIX}:{suffix}"); + let skip_key = format!("{PLAN_SKIP_KEY_PREFIX}:{suffix}"); + let outcome = match self.read::(kv, &plan_key, work).await { + Ok(Some(CacheEntry::Plan { + plan, manifest_url, .. + })) => { + if api::validate_plan(&plan, manifest_url.as_deref(), article.scalars).is_err() { + self.delete_invalid_entry(kv, &plan_key, work).await; + return Err(Reason::CachedPlanInvalid); + } + return render( + html, + article, + &plan, + manifest_url.as_deref(), + self.max_document_bytes, + ) + .map(|signed| (signed, Reason::CacheHit)) + .map_err(render_reason); + } + Ok(Some(CacheEntry::Skip { .. })) => { + self.delete_invalid_entry(kv, &plan_key, work).await; + return Err(Reason::CachedEntryInvalid); + } + Err(Reason::Timeout) => Err(Reason::Timeout), + Err(reason) => return Err(reason), + Ok(None) => match self.read::(kv, &skip_key, work).await { + Ok(Some(CacheEntry::Skip { .. })) => return Err(Reason::CachedSkip), + Ok(Some(CacheEntry::Plan { .. })) => { + self.delete_invalid_entry(kv, &skip_key, work).await; + return Err(Reason::CachedEntryInvalid); + } + Err(Reason::Timeout) => Err(Reason::Timeout), + Err(reason) => return Err(reason), + Ok(None) => self + .request_plan(request, services, html, article, policy_digest, work) + .await + .and_then(|(plan, manifest_url)| { + api::validate_plan(&plan, manifest_url.as_deref(), article.scalars) + .map_err(|_| Reason::InvalidPlan)?; + let signed = render( + html, + article, + &plan, + manifest_url.as_deref(), + self.max_document_bytes, + ) + .map_err(render_reason)?; + Ok((signed, plan, manifest_url)) + }), + }, + }; + match outcome { + Ok((signed, plan, manifest_url)) => { + // A plan write that runs out of time is abandoned; the page is still signed. + let entry = CacheEntry::Plan { + plan, + manifest_url, + expires_at: Self::expires_at(self.config.plan_ttl_seconds), + ttl_less: false, + }; + self.write(kv, &plan_key, entry, self.config.plan_ttl_seconds, work) + .await; + Ok((signed, Reason::Signed)) + } + Err(reason) => { + let mut deadline = Self::skip_deadline(services); + if matches!(reason, Reason::PolicyChanged | Reason::PolicyDigestMismatch) { + self.evict_policy(kv, &mut deadline).await; + } + self.write_skip(kv, &skip_key, reason, &mut deadline).await; + Err(reason) + } + } + } + + async fn policy_digest( + &self, + page_url: &str, + services: &RuntimeServices, + kv: &dyn PlatformKvStore, + work: &mut Deadline<'_>, + ) -> Result { + match self.read::(kv, &self.policy_key, work).await { + Ok(Some(PolicyEntry::Digest { policy_digest, .. })) + if api::valid_policy_digest(&policy_digest) => + { + return Ok(policy_digest); + } + Ok(Some(PolicyEntry::Digest { .. })) | Ok(Some(PolicyEntry::Skip { .. })) => { + self.delete_invalid_entry(kv, &self.policy_key, work).await; + return Err(Reason::CachedEntryInvalid); + } + // Nothing can be cached without a store. + Err(Reason::KvUnavailable) => return Err(Reason::KvUnavailable), + Err(reason) => return Err(reason), + Ok(None) => {} + } + match self + .read::(kv, &self.policy_skip_key, work) + .await + { + Ok(Some(PolicyEntry::Skip { .. })) => return Err(Reason::CachedSkip), + Ok(Some(PolicyEntry::Digest { .. })) => { + self.delete_invalid_entry(kv, &self.policy_skip_key, work) + .await; + return Err(Reason::CachedEntryInvalid); + } + Err(Reason::KvUnavailable) => return Err(Reason::KvUnavailable), + Err(reason) => return Err(reason), + Ok(None) => {} + } + + let reason = match self.request_policy(page_url, services, work).await { + Ok(policy_digest) => { + let entry = PolicyEntry::Digest { + policy_digest: policy_digest.clone(), + expires_at: Self::expires_at(self.config.policy_ttl_seconds), + ttl_less: false, + }; + self.write( + kv, + &self.policy_key, + entry, + self.config.policy_ttl_seconds, + work, + ) + .await; + if !work.expired() { + return Ok(policy_digest); + } + // The digest is not cached, so the next view could not find a + // plan skip written under it; skip at the policy level instead. + Reason::Timeout + } + Err(reason) => reason, + }; + let ttl = self.policy_skip_ttl(); + let entry = PolicyEntry::Skip { + reason, + expires_at: Self::expires_at(ttl), + ttl_less: false, + }; + self.write( + kv, + &self.policy_skip_key, + entry, + ttl, + &mut Self::skip_deadline(services), + ) + .await; + Err(reason) + } + + fn policy_skip_ttl(&self) -> u64 { + self.config + .policy_ttl_seconds + .min(self.config.negative_ttl_seconds) + } + + async fn request_policy( + &self, + page_url: &str, + services: &RuntimeServices, + work: &mut Deadline<'_>, + ) -> Result { + let mut url = url::Url::parse(&self.policy_url).map_err(|_| Reason::RequestBuild)?; + url.query_pairs_mut().append_pair("page_url", page_url); + let body = self + .fetch( + services, + Method::GET, + url.as_str(), + EdgeBody::empty(), + api::MAX_POLICY_RESPONSE_BYTES, + work, + ) + .await?; + api::parse_policy_response(&body).map_err(|error| parse_error_reason(&error)) + } + + async fn evict_policy(&self, kv: &dyn PlatformKvStore, deadline: &mut Deadline<'_>) { + if !self.delete_cache_key(kv, &self.policy_key, deadline).await { + log::debug!("encypher: policy digest eviction failed"); + } + if !self + .delete_cache_key(kv, &self.policy_skip_key, deadline) + .await + { + log::debug!("encypher: policy skip eviction failed"); + } + } + + /// Call the sign API once. Every failure maps to a reason code. + async fn request_plan( + &self, + request: &PageRequest, + services: &RuntimeServices, + html: &str, + article: &Article, + policy_digest: &str, + work: &mut Deadline<'_>, + ) -> Result<(EmbeddingPlan, Option), Reason> { + let title = text::title_from_range(html, article.title_range); + let body = serde_json::to_vec(&SignRequest { + text: &article.text, + page_url: &request.page_url, + document_title: title.as_deref(), + boundary_selector: &article.boundary.selector, + expected_policy_digest: policy_digest, + }) + .map_err(|_| Reason::RequestBuild)?; + let body = self + .fetch( + services, + Method::POST, + &self.sign_url, + EdgeBody::from(body), + api::MAX_RESPONSE_BYTES, + work, + ) + .await?; + let parsed = api::parse_sign_response(&body).map_err(|error| parse_error_reason(&error))?; + if !parsed.success { + return Err(if parsed.error.as_deref() == Some("quota_exceeded") { + Reason::QuotaExceeded + } else { + Reason::SignRejected + }); + } + if parsed.policy_digest.as_deref() != Some(policy_digest) { + return Err(Reason::PolicyDigestMismatch); + } + let plan = parsed.embedding_plan.ok_or(Reason::NoPlan)?; + Ok((plan, parsed.manifest_url)) + } + + /// The worker token header value, marked sensitive so no log shows it. + fn worker_token(&self) -> Result { + let mut token = HeaderValue::from_str(self.config.worker_token.expose().trim()) + .map_err(|_| Reason::RequestBuild)?; + token.set_sensitive(true); + Ok(token) + } + + /// One bounded JSON HTTP call; never retried. + async fn fetch( + &self, + services: &RuntimeServices, + method: Method, + url: &str, + body: EdgeBody, + max_response_bytes: usize, + work: &mut Deadline<'_>, + ) -> Result { + let http_request = Request::builder() + .method(method) + .uri(url) + .header(header::CONTENT_TYPE, "application/json") + .header(WORKER_TOKEN_HEADER, self.worker_token()?) + .body(body) + .map_err(|_| Reason::RequestBuild)?; + let response_limit = max_response_bytes.saturating_add(1); + let response = self + .send_api(services, http_request, response_limit, work) + .await?; + let status = response.status(); + if status != StatusCode::OK { + return Err(match status.as_u16() { + 403 => Reason::Forbidden, + 409 => Reason::PolicyChanged, + 422 => Reason::Unprocessable, + 429 => Reason::RateLimited, + 500..=599 => Reason::ServerError, + _ => Reason::UnexpectedStatus, + }); + } + // One byte over the limit is enough for the parser to report TooLarge. + match work + .race(response.into_body().into_bytes_bounded(response_limit)) + .await + { + None => Err(Reason::Timeout), + Some(Err(_)) => Err(Reason::ResponseTooLarge), + Some(Ok(body)) => Ok(body), + } + } + + /// Send one request to the API backend, bounded by the first-byte + /// timeout and `work`, reading at most `response_limit` body bytes. + async fn send_api( + &self, + services: &RuntimeServices, + http_request: Request, + response_limit: usize, + work: &mut Deadline<'_>, + ) -> Result, Reason> { + let backend = ensure_integration_backend_with_timeout( + services, + &self.config.api_base_url, + INTEGRATION_ID, + Duration::from_millis(self.config.timeout_ms), + ) + .map_err(|_| Reason::Backend)?; + let platform_request = PlatformHttpRequest::new(http_request, backend) + .with_cache_bypass() + .with_max_response_body_bytes(response_limit); + match work + .race(services.http_client().send(platform_request)) + .await + { + None => Err(Reason::Timeout), + Some(Err(_)) => Err(Reason::HttpError), + Some(Ok(response)) => Ok(response.response), + } + } + + /// Read a fresh entry. Expired entries are misses; in TTL-less write mode + /// they are also deleted, best effort, so the store cleans itself. + async fn read( + &self, + kv: &dyn PlatformKvStore, + key: &str, + deadline: &mut Deadline<'_>, + ) -> Result, Reason> { + let bytes = match deadline.race(kv.get_bytes(key)).await { + None => return Err(Reason::Timeout), + Some(Err(KvError::Unavailable)) => return Err(Reason::KvUnavailable), + Some(Err(_)) => return Err(Reason::KvError), + Some(Ok(None)) => return Ok(None), + Some(Ok(Some(bytes))) => bytes, + }; + let entry: T = match serde_json::from_slice(&bytes) { + Ok(entry) => entry, + Err(_) => { + self.delete_invalid_entry(kv, key, deadline).await; + return Err(Reason::CachedEntryInvalid); + } + }; + // An entry written without a TTL shows the mode to an instance that has + // not written yet, as on Spin, which instantiates the module per request. + if entry.ttl_less() { + self.ttl_less.store(true, Ordering::Relaxed); + } + if is_fresh(entry.expires_at(), unix_now()) { + return Ok(Some(entry)); + } + if self.ttl_less.load(Ordering::Relaxed) && !self.delete_cache_key(kv, key, deadline).await + { + log::debug!("encypher: expired cache entry could not be deleted"); + } + Ok(None) + } + + /// Best-effort cache deletion, bounded by the caller's existing deadline. + async fn delete_cache_key( + &self, + kv: &dyn PlatformKvStore, + key: &str, + deadline: &mut Deadline<'_>, + ) -> bool { + matches!(deadline.race(kv.delete(key)).await, Some(Ok(()))) + } + + /// Delete a malformed or structurally invalid entry without masking fail-open. + async fn delete_invalid_entry( + &self, + kv: &dyn PlatformKvStore, + key: &str, + deadline: &mut Deadline<'_>, + ) { + if !self.delete_cache_key(kv, key, deadline).await { + log::debug!("encypher: invalid cache entry could not be deleted"); + } + } + + /// Best-effort cache write; failures are logged and ignored. + /// + /// Writes carry a TTL until the store first reports TTL writes as + /// unsupported; from then on this instance writes without one, and marks + /// the entry so a later instance that reads it knows the mode too. + async fn write( + &self, + kv: &dyn PlatformKvStore, + key: &str, + mut entry: E, + ttl_seconds: u64, + deadline: &mut Deadline<'_>, + ) { + if !self.ttl_less.load(Ordering::Relaxed) { + let Some(value) = encode(&entry) else { + return; + }; + let ttl = Duration::from_secs(ttl_seconds); + match deadline.race(kv.put_bytes_with_ttl(key, value, ttl)).await { + Some(Ok(())) => return, + Some(Err(KvError::Unsupported { .. })) => { + self.ttl_less.store(true, Ordering::Relaxed); + log::info!( + "encypher: the KV store does not support TTL writes; entries expire by their expires_at" + ); + } + Some(Err(_)) => { + log::debug!("encypher: cache write failed"); + return; + } + None => { + log::debug!("encypher: cache write timed out"); + return; + } + } + } + entry.mark_ttl_less(); + let Some(value) = encode(&entry) else { + return; + }; + match deadline.race(kv.put_bytes(key, value)).await { + Some(Ok(())) => {} + Some(Err(_)) => log::debug!("encypher: cache write failed"), + None => log::debug!("encypher: cache write timed out"), + } + } + + /// Write a negative article entry under its separate skip key. + async fn write_skip( + &self, + kv: &dyn PlatformKvStore, + key: &str, + reason: Reason, + deadline: &mut Deadline<'_>, + ) { + let ttl = self.config.negative_ttl_seconds; + let entry = CacheEntry::Skip { + reason, + expires_at: Self::expires_at(ttl), + ttl_less: false, + }; + self.write(kv, key, entry, ttl, deadline).await; + } +} + +/// Serialize a cache entry; a failure is logged and the write skipped. +fn encode(entry: &impl Serialize) -> Option { + match serde_json::to_vec(entry) { + Ok(value) => Some(Bytes::from(value)), + Err(_) => { + log::debug!("encypher: cache entry could not be serialized"); + None + } + } +} + +/// Apply a validated plan and optional manifest in one exact-sized output. +fn render( + html: &str, + article: &Article, + plan: &EmbeddingPlan, + manifest_url: Option<&str>, + max_output_bytes: usize, +) -> Result { + let manifest = manifest_url + .filter(|url| !url.is_empty()) + .zip(article.head_close); + embed::apply_plan(html, &article.boundary, plan, manifest, max_output_bytes) +} + +fn render_reason(error: embed::ApplyError) -> Reason { + match error { + embed::ApplyError::Alignment => Reason::AlignmentFailed, + embed::ApplyError::TooLarge => Reason::OutputTooLarge, + } +} + +fn parse_error_reason(error: &ApiError) -> Reason { + match error { + ApiError::TooLarge => Reason::ResponseTooLarge, + ApiError::Malformed => Reason::MalformedResponse, + } +} + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + use std::collections::HashMap; + use std::sync::atomic::AtomicUsize; + use std::sync::{LazyLock, Mutex}; + use std::task::Poll; + + use error_stack::Report; + use serde_json::json; + + use super::*; + use trusted_server_core::platform::test_support::{ + StoppedTimer, StubHttpClient, build_services_with_http_client, + }; + use trusted_server_core::platform::{ + PlatformCacheIntent, PlatformError, PlatformHttpClient, PlatformPendingRequest, + PlatformResponse, PlatformSelectResult, declare_platform_after_response_handoff, + take_after_response_requests, + }; + + const TOKEN: &str = "cwk_test_worker_token_value"; + const PAGE_URL: &str = "https://www.publisher.example.com/news/harbor-vote"; + const MARKER: &str = "\u{E0100}\u{E0101}\u{FE01}"; + const MANIFEST_URL: &str = "https://api.example.com/manifests/doc-1"; + const POLICY_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const ARTICLE: &str = "Harbor Council Votes\ +

Harbor council votes on the ferry schedule

\ +

The harbor council met on Tuesday evening and approved a revised ferry schedule \ + for the coming season.

Residents asked for earlier departures on weekdays.

\ +
Footer
"; + /// Simulated time that passes each time a [`TickTimer`] sleep is polled. + const TICK: Duration = Duration::from_millis(100); + + // --- test doubles ------------------------------------------------------- + + /// Return `Pending` `times` times, waking the task each time, then `Ready`. + async fn yield_pending(times: usize) { + for _ in 0..times { + let mut yielded = false; + futures::future::poll_fn(|cx| { + if yielded { + Poll::Ready(()) + } else { + yielded = true; + cx.waker().wake_by_ref(); + Poll::Pending + } + }) + .await; + } + } + + /// A timer on simulated time: each poll of a sleep advances it by [`TICK`]. + /// Sleeps only advance while the work they race is pending, as on a real + /// runtime where the clock runs while I/O is outstanding. + #[derive(Default)] + struct TickTimer { + requested: Mutex>, + } + + #[async_trait::async_trait(?Send)] + impl PlatformTimer for TickTimer { + async fn sleep(&self, duration: Duration) { + self.requested + .lock() + .expect("should lock requested durations") + .push(duration); + let ticks = duration.as_millis().div_ceil(TICK.as_millis()); + yield_pending(usize::try_from(ticks).expect("ticks should fit")).await; + } + } + + /// In-memory KV store that counts calls and can fail, stall or refuse TTLs. + #[derive(Default)] + struct MemoryKv { + entries: Mutex>, + ttls: Mutex>, + deletes: Mutex>, + reads: AtomicUsize, + ttl_writes: AtomicUsize, + /// Plan-key reads that report a miss before reads see the stored entry. + hidden_reads: AtomicUsize, + /// Every read fails with [`KvError::Unavailable`], as an unbound store does. + unavailable: bool, + /// Every read fails with an error other than unavailability. + fail_reads: bool, + /// Reads of keys with this prefix fail with an error other than + /// unavailability; every other read succeeds. + fail_reads_of: Option<&'static str>, + /// Every read never completes. + hung_reads: bool, + /// Writes to keys with this prefix never complete. + hung_writes_to: Option<&'static str>, + /// The first write to this exact key never completes; later writes succeed. + hung_first_write_to: Option, + hung_first_write_taken: AtomicBool, + /// TTL writes fail with [`KvError::Unsupported`], as on Spin. + ttl_unsupported: bool, + /// `Pending` polls before every operation completes. + pending_polls: usize, + /// Positive entry inserted after a negative writer's final read but before its put. + concurrent_positive_before_skip_write: Mutex>, + } + + impl MemoryKv { + fn entries(&self) -> HashMap { + self.entries.lock().expect("should lock entries").clone() + } + + fn entry(&self, key: &str) -> serde_json::Value { + serde_json::from_slice(&self.entries()[key]).expect("should store JSON") + } + + fn insert(&self, key: &str, value: &serde_json::Value) { + self.entries.lock().expect("should lock entries").insert( + key.to_owned(), + Bytes::from(serde_json::to_vec(value).expect("should serialize")), + ); + } + + fn insert_raw(&self, key: &str, value: impl Into) { + self.entries + .lock() + .expect("should lock entries") + .insert(key.to_owned(), value.into()); + } + + fn entry_bytes(&self, key: &str) -> Bytes { + self.entries()[key].clone() + } + + fn remove(&self, key: &str) { + self.entries + .lock() + .expect("should lock entries") + .remove(key); + } + + /// The single article entry under either current article namespace. + fn only_entry(&self) -> (String, serde_json::Value) { + let entries: Vec<_> = self + .entries() + .into_keys() + .filter(|key| { + key.starts_with(PLAN_KEY_PREFIX) || key.starts_with(PLAN_SKIP_KEY_PREFIX) + }) + .collect(); + assert_eq!(entries.len(), 1, "should hold exactly one article entry"); + let key = entries.into_iter().next().expect("should have an entry"); + let value = self.entry(&key); + (key, value) + } + + fn reads(&self) -> usize { + self.reads.load(Ordering::SeqCst) + } + + async fn store(&self, key: &str, value: Bytes) -> Result<(), KvError> { + yield_pending(self.pending_polls).await; + if self + .hung_writes_to + .is_some_and(|prefix| key.starts_with(prefix)) + || (self.hung_first_write_to.as_deref() == Some(key) + && !self.hung_first_write_taken.swap(true, Ordering::SeqCst)) + { + futures::future::pending::<()>().await; + } + let writes_skip = serde_json::from_slice::(&value) + .ok() + .and_then(|entry| { + entry + .get("kind") + .and_then(|kind| kind.as_str()) + .map(str::to_owned) + }) + .as_deref() + == Some("skip"); + if writes_skip + && let Some((positive_key, positive)) = self + .concurrent_positive_before_skip_write + .lock() + .expect("should lock concurrent write") + .take() + { + self.entries + .lock() + .expect("should lock entries") + .insert(positive_key, positive); + } + self.entries + .lock() + .expect("should lock entries") + .insert(key.to_owned(), value); + Ok(()) + } + } + + #[async_trait::async_trait(?Send)] + impl PlatformKvStore for MemoryKv { + async fn get_bytes(&self, key: &str) -> Result, KvError> { + self.reads.fetch_add(1, Ordering::SeqCst); + yield_pending(self.pending_polls).await; + if self.hung_reads { + futures::future::pending::<()>().await; + } + if self.unavailable { + return Err(KvError::Unavailable); + } + if self.fail_reads + || self + .fail_reads_of + .is_some_and(|prefix| key.starts_with(prefix)) + { + return Err(KvError::LimitExceeded { + message: "test failure".to_owned(), + }); + } + if key.starts_with(PLAN_KEY_PREFIX) + && self + .hidden_reads + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1)) + .is_ok() + { + return Ok(None); + } + Ok(self + .entries + .lock() + .expect("should lock entries") + .get(key) + .cloned()) + } + + async fn put_bytes(&self, key: &str, value: Bytes) -> Result<(), KvError> { + self.store(key, value).await + } + + async fn put_bytes_with_ttl( + &self, + key: &str, + value: Bytes, + ttl: Duration, + ) -> Result<(), KvError> { + self.ttl_writes.fetch_add(1, Ordering::SeqCst); + if self.ttl_unsupported { + return Err(KvError::Unsupported { + operation: "put_bytes_with_ttl".to_owned(), + }); + } + self.ttls + .lock() + .expect("should lock TTLs") + .insert(key.to_owned(), ttl); + self.store(key, value).await + } + + async fn delete(&self, key: &str) -> Result<(), KvError> { + yield_pending(self.pending_polls).await; + self.deletes + .lock() + .expect("should lock deletes") + .push(key.to_owned()); + self.remove(key); + Ok(()) + } + + async fn list_keys_page( + &self, + _prefix: &str, + _cursor: Option<&str>, + _limit: usize, + ) -> Result { + Ok(edgezero_core::key_value_store::KvPage::default()) + } + } + + /// HTTP client whose send never completes. + #[derive(Default)] + struct HungHttpClient { + sends: AtomicUsize, + } + + #[async_trait::async_trait(?Send)] + impl PlatformHttpClient for HungHttpClient { + async fn send( + &self, + _request: PlatformHttpRequest, + ) -> Result> { + self.sends.fetch_add(1, Ordering::SeqCst); + futures::future::pending().await + } + + async fn send_async( + &self, + _request: PlatformHttpRequest, + ) -> Result> { + Err(Report::new(PlatformError::HttpClient)) + } + + async fn select( + &self, + _pending_requests: Vec, + ) -> Result> { + Err(Report::new(PlatformError::HttpClient)) + } + } + + /// Wraps [`StubHttpClient`] so the send and the response body each return + /// `Pending` before they are ready, as on Cloudflare, Spin and Axum. + struct YieldingHttpClient { + inner: Arc, + pending_polls: usize, + } + + #[async_trait::async_trait(?Send)] + impl PlatformHttpClient for YieldingHttpClient { + async fn send( + &self, + request: PlatformHttpRequest, + ) -> Result> { + yield_pending(self.pending_polls).await; + let response = self.inner.send(request).await?; + let (parts, body) = response.response.into_parts(); + let bytes = body + .into_bytes_bounded(usize::MAX) + .await + .expect("should read the stub body"); + let polls = self.pending_polls; + let body = EdgeBody::stream(futures::stream::once(async move { + yield_pending(polls).await; + bytes + })); + Ok(PlatformResponse::new(http::Response::from_parts( + parts, body, + ))) + } + + async fn send_async( + &self, + _request: PlatformHttpRequest, + ) -> Result> { + Err(Report::new(PlatformError::HttpClient)) + } + + async fn select( + &self, + _pending_requests: Vec, + ) -> Result> { + Err(Report::new(PlatformError::HttpClient)) + } + } + + /// Records log lines, with their level, emitted on the current thread. + struct ThreadLogger; + + thread_local! { + static LOGS: RefCell> = const { RefCell::new(Vec::new()) }; + } + + impl log::Log for ThreadLogger { + fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool { + true + } + + fn log(&self, record: &log::Record<'_>) { + LOGS.with(|logs| { + logs.borrow_mut() + .push(format!("{} {}", record.level(), record.args())); + }); + } + + fn flush(&self) {} + } + + static LOGGER: ThreadLogger = ThreadLogger; + + /// A sign-failure case: name and the API response, `None` for a transport error. + type FailureCase = (&'static str, Option<(u16, Vec)>); + /// A gate case: name, signer, and the request mutation that trips the gate. + type GateCase<'a> = (&'static str, &'a Arc, fn(&mut PageRequest)); + + /// Runs `run` with this thread's log lines captured. Returns `None`, after + /// skipping `run`, when another logger already owns the process. + fn capture_logs(run: impl FnOnce()) -> Option> { + static INSTALLED: LazyLock = LazyLock::new(|| { + let installed = log::set_logger(&LOGGER).is_ok(); + if installed { + log::set_max_level(log::LevelFilter::Trace); + } + installed + }); + if !*INSTALLED { + return None; + } + LOGS.with(|logs| logs.borrow_mut().clear()); + run(); + Some(LOGS.with(RefCell::take)) + } + + // --- fixtures ----------------------------------------------------------- + + fn config_json(token: &str) -> serde_json::Value { + json!({ + "api_base_url": "https://api.example.com", + "domain": "publisher.example.com", + "worker_token": token, + }) + } + + fn factory_with(config: serde_json::Value, max_document_bytes: usize) -> Arc { + let config: EncypherConfig = serde_json::from_value(config).expect("should parse config"); + Arc::new(Signer::new(config, max_document_bytes)) + } + + fn factory() -> Arc { + factory_with(config_json(TOKEN), 5_000_000) + } + + fn article_with_selector(html: &str, selector: Option<&str>) -> Article { + let boundary = + boundary::find_article_boundary(html, selector).expect("should find boundary"); + let metadata = text::document_metadata(html); + let boundary_html = html + .get(boundary.start..boundary.end) + .expect("boundary should be valid"); + let (text, scalars) = text::visible_text(boundary_html, api::MAX_TEXT_SCALARS); + Article { + boundary, + text, + scalars, + head_close: metadata.head_close, + title_range: metadata.title_range, + } + } + + fn policy_key() -> String { + factory().policy_key.clone() + } + + fn policy_skip_key() -> String { + factory().policy_skip_key.clone() + } + + /// Services without a timer, as on Fastly. + fn services(http: Arc, kv: &Arc) -> RuntimeServices { + build_services_with_http_client(http) + .with_kv_store(Arc::clone(kv) as Arc) + } + + /// Services with a [`TickTimer`], as on Cloudflare, Spin and Axum. + fn timed_services(http: Arc, kv: &Arc) -> RuntimeServices { + services(http, kv).with_timer(Arc::new(TickTimer::default())) + } + + fn request() -> PageRequest { + PageRequest { + method: Method::GET, + page_url: PAGE_URL.to_owned(), + query: String::new(), + removed_prefix: String::new(), + response_status: 200, + origin_response_is_private: false, + } + } + + fn plan_json(marker: &str) -> serde_json::Value { + json!({ + "index_unit": "codepoint", + "operations": [{ "insert_after_index": 5, "marker": marker }], + }) + } + + fn sign_response(marker: &str) -> Vec { + sign_response_for_policy(marker, POLICY_DIGEST) + } + + fn policy_response(digest: &str) -> Vec { + serde_json::to_vec(&json!({"policy_digest": digest})).expect("should serialize policy") + } + + fn sign_response_for_policy(marker: &str, digest: &str) -> Vec { + serde_json::to_vec(&json!({ + "success": true, + "policy_digest": digest, + "embedding_plan": plan_json(marker), + "manifest_url": MANIFEST_URL, + "verification_url": "https://api.example.com/verify/doc-1", + "document_id": "doc-1", + })) + .expect("should serialize sign response") + } + + fn signing_http() -> Arc { + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + http.push_response(200, sign_response(MARKER)); + http + } + + fn seed_plan(kv: &Arc) -> String { + view(&factory(), &services(signing_http(), kv)) + } + + /// Drive one document the way the middleware and the publisher driver do: + /// the gates on the request, when there is one, then two chunks, then the + /// asynchronous finish with the core backstop. A request the gates refuse, + /// or none, holds nothing, so the document passes through. + fn run( + factory: &Arc, + request: Option, + services: &RuntimeServices, + html: &str, + ) -> String { + let Some(request) = request.and_then(|request| factory.admit(request).ok()) else { + return html.to_owned(); + }; + futures::executor::block_on(async { + let mut processor = document_processor(factory, request, None); + let (first, second) = html.as_bytes().split_at(html.len() / 2); + let mut output = processor + .process_chunk(first, false) + .expect("should process first chunk"); + output.extend( + processor + .finish(second, services) + .await + .expect("should finish the document"), + ); + String::from_utf8(output).expect("should emit UTF-8") + }) + } + + /// One view of [`ARTICLE`] at [`PAGE_URL`]. + fn view(factory: &Arc, services: &RuntimeServices) -> String { + run(factory, Some(request()), services, ARTICLE) + } + + fn assert_signed(output: &str) { + assert!(output.contains(MARKER), "should embed the plan marker"); + assert!( + output.contains("c2pa-manifest-url") && output.contains(MANIFEST_URL), + "should inject the manifest meta tag" + ); + assert_eq!( + output.matches(MARKER).count(), + 1, + "should add the marker once" + ); + } + + /// Rewrite the `expires_at` of the entry at `key` to `expires_at`. + fn set_expiry(kv: &MemoryKv, key: &str, expires_at: u64) { + let mut entry = kv.entry(key); + entry["expires_at"] = json!(expires_at); + kv.insert(key, &entry); + } + + fn expired() -> u64 { + unix_now().saturating_sub(1) + } + + // --- policy ------------------------------------------------------------- + + #[test] + fn policy_failures_are_negatively_cached_without_using_an_old_plan() { + let cases: Vec = vec![ + ("unavailable", Some((503, Vec::new()))), + ("forbidden", Some((403, Vec::new()))), + ("transport", None), + ("malformed", Some((200, b"not json".to_vec()))), + ("missing digest", Some((200, b"{}".to_vec()))), + ( + "uppercase", + Some((200, policy_response(&POLICY_DIGEST.to_uppercase()))), + ), + ( + "oversized", + Some((200, vec![b' '; api::MAX_POLICY_RESPONSE_BYTES + 2])), + ), + ]; + for (case, response) in cases { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + kv.remove(&policy_key()); + let http = Arc::new(StubHttpClient::new()); + if let Some((status, body)) = response { + http.push_response(status, body); + } + let services = services(Arc::clone(&http) as Arc, &kv); + for _ in 0..2 { + assert_eq!(view(&factory(), &services), ARTICLE, "{case}"); + } + assert_eq!(http.recorded_request_methods(), ["GET"], "{case}"); + assert_eq!(kv.entry(&policy_skip_key())["kind"], "skip", "{case}"); + assert_eq!( + kv.ttls.lock().expect("lock")[&policy_skip_key()], + Duration::from_secs(300) + ); + // Once the skip is gone, refresh may reuse only the matching old plan. + kv.remove(&policy_skip_key()); + http.push_response(200, policy_response(POLICY_DIGEST)); + assert_signed(&view(&factory(), &services)); + assert_eq!(http.recorded_request_methods(), ["GET", "GET"]); + } + } + + #[test] + fn malformed_cached_policy_fails_closed_for_one_view_then_recovers() { + let future = unix_now() + 300; + for value in [ + json!("not an entry"), + json!({"kind": "digest", "policy_digest": null, "expires_at": future}), + json!({"kind": "digest", "policy_digest": "bad", "expires_at": future}), + json!({ + "kind": "digest", "policy_digest": POLICY_DIGEST, "expires_at": future, + "extra": true, + }), + // `expires_at` is required. + json!({"kind": "digest", "policy_digest": POLICY_DIGEST}), + ] { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + let key = policy_key(); + kv.insert(&key, &value); + let http = Arc::new(StubHttpClient::new()); + let services = services(Arc::clone(&http) as Arc, &kv); + + assert_eq!(view(&factory(), &services), ARTICLE, "{value}"); + assert!(http.recorded_request_methods().is_empty(), "{value}"); + assert!( + kv.deletes.lock().expect("lock deletes").contains(&key), + "{value}: malformed policy should be evicted" + ); + assert!( + !kv.entries().contains_key(&key), + "{value}: malformed policy should not remain" + ); + + http.push_response(200, policy_response(POLICY_DIGEST)); + assert_signed(&view(&factory(), &services)); + assert_eq!(http.recorded_request_methods(), ["GET"], "{value}"); + } + } + + #[test] + fn policy_cache_hit_and_article_or_policy_changes_select_the_correct_plan() { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, sign_response(MARKER)); + let services = services(Arc::clone(&http) as Arc, &kv); + let changed = ARTICLE.replace("Tuesday", "Wednesday"); + assert_signed(&run(&factory(), Some(request()), &services, &changed)); + assert_eq!(http.recorded_request_methods(), ["POST"]); + + kv.remove(&policy_key()); + let next_digest = "b".repeat(64); + http.push_response(200, policy_response(&next_digest)); + http.push_response(200, sign_response_for_policy(MARKER, &next_digest)); + assert_signed(&view(&factory(), &services)); + assert_eq!(http.recorded_request_methods(), ["POST", "GET", "POST"]); + let count = http.recorded_request_methods().len(); + assert_signed(&view(&factory(), &services)); + assert_eq!(http.recorded_request_methods().len(), count); + // Returning to the original policy selects its original plan, not the intervening one. + kv.remove(&policy_key()); + http.push_response(200, policy_response(POLICY_DIGEST)); + assert_signed(&view(&factory(), &services)); + assert_eq!( + http.recorded_request_methods(), + ["POST", "GET", "POST", "GET"] + ); + } + + #[test] + fn conflict_or_response_digest_mismatch_evicts_policy_without_retrying() { + let mut missing: serde_json::Value = + serde_json::from_slice(&sign_response(MARKER)).expect("JSON"); + missing + .as_object_mut() + .expect("object") + .remove("policy_digest"); + for (status, body) in [ + ( + 409, + br#"{"detail":"TRUSTED_SERVER_POLICY_CHANGED"}"#.to_vec(), + ), + (409, b"unparseable conflict".to_vec()), + (200, sign_response_for_policy(MARKER, &"b".repeat(64))), + (200, sign_response_for_policy(MARKER, "invalid")), + (200, serde_json::to_vec(&missing).expect("JSON")), + ] { + let kv = Arc::new(MemoryKv::default()); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + http.push_response(status, body); + let services = services(Arc::clone(&http) as Arc, &kv); + assert_eq!(view(&factory(), &services), ARTICLE); + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + assert!(!kv.entries().contains_key(&policy_key())); + assert_eq!(kv.only_entry().1["kind"], "skip"); + // Next request refetches policy, but the failed article remains negatively cached. + http.push_response(200, policy_response(POLICY_DIGEST)); + assert_eq!(view(&factory(), &services), ARTICLE); + assert_eq!(http.recorded_request_methods(), ["GET", "POST", "GET"]); + } + } + + #[test] + fn entries_expire_at_their_ttl_and_the_store_ttl_matches() { + let kv = Arc::new(MemoryKv::default()); + let before = unix_now(); + seed_plan(&kv); + let after = unix_now(); + let ttls = kv.ttls.lock().expect("lock").clone(); + let (plan_key, plan) = kv.only_entry(); + for (key, entry, ttl) in [ + (policy_key(), kv.entry(&policy_key()), 300), + (plan_key, plan, 604_800), + ] { + assert_eq!(ttls[&key], Duration::from_secs(ttl)); + let expires_at = entry["expires_at"] + .as_u64() + .expect("should carry expires_at"); + assert!( + (before + ttl..=after + ttl).contains(&expires_at), + "{key} should expire {ttl}s after it was written" + ); + } + for (policy_ttl, negative_ttl, expected) in [(60, 300, 60), (300, 60, 60)] { + let kv = Arc::new(MemoryKv::default()); + let http = Arc::new(StubHttpClient::new()); + http.push_response(503, Vec::new()); + let mut config = config_json(TOKEN); + config["policy_ttl_seconds"] = json!(policy_ttl); + config["negative_ttl_seconds"] = json!(negative_ttl); + let factory = factory_with(config, 5_000_000); + assert_eq!(view(&factory, &services(http, &kv)), ARTICLE); + assert_eq!( + kv.ttls.lock().expect("lock")[&factory.policy_skip_key], + Duration::from_secs(expected) + ); + } + } + + // --- sign path and cache ------------------------------------------------ + + #[test] + fn signs_on_a_miss_with_the_worker_token_and_caches_the_plan() { + let http = signing_http(); + let kv = Arc::new(MemoryKv::default()); + let services = services(Arc::clone(&http) as Arc, &kv); + + let output = view(&factory(), &services); + + assert_signed(&output); + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + assert_eq!( + http.recorded_cache_intents(), + [PlatformCacheIntent::Bypass, PlatformCacheIntent::Bypass], + "credential-bound policy and sign responses must bypass intermediary caches" + ); + assert_eq!( + http.recorded_request_uris(), + [ + format!( + "https://api.example.com/api/v1/public/cdn/trusted-server/policy?page_url={}", + url::form_urlencoded::byte_serialize(PAGE_URL.as_bytes()).collect::() + ), + "https://api.example.com/api/v1/public/cdn/sign".to_owned(), + ] + ); + let headers = &http.recorded_request_headers()[1]; + let header = |name: &str| { + headers + .iter() + .find(|(key, _)| key == name) + .map(|(_, value)| value.as_str()) + }; + assert_eq!(header("x-encypher-worker-token"), Some(TOKEN)); + assert_eq!(header("content-type"), Some("application/json")); + assert_eq!( + header("idempotency-key"), + None, + "should send no idempotency key" + ); + let body: serde_json::Value = serde_json::from_slice(&http.recorded_request_bodies()[1]) + .expect("should send a JSON body"); + assert_eq!(body["page_url"], PAGE_URL); + assert_eq!(body["expected_policy_digest"], POLICY_DIGEST); + assert_eq!(body["document_title"], "Harbor Council Votes"); + assert_eq!(body["boundary_selector"], "article"); + let sent_text = body["text"].as_str().expect("should send text"); + assert!( + sent_text.starts_with("Harbor council votes on the ferry schedule The harbor council"), + "should send the article's visible text, got {sent_text}" + ); + assert!(!sent_text.contains("Footer") && !sent_text.contains("Home")); + let (key, entry) = kv.only_entry(); + assert!( + key.starts_with("encypher:plan:v4:") && key.contains(POLICY_DIGEST), + "unexpected cache key {key}" + ); + assert!(!key.contains(TOKEN), "cache key must not contain the token"); + assert!( + policy_key().starts_with("encypher:policy:v3:"), + "unexpected policy key" + ); + assert_eq!(entry["kind"], "plan"); + assert_eq!(entry["plan"], plan_json(MARKER)); + } + + #[test] + fn a_fresh_plan_that_expands_past_the_document_budget_passes_through() { + let http = signing_http(); + let kv = Arc::new(MemoryKv::default()); + let signer = factory_with(config_json(TOKEN), ARTICLE.len()); + + let output = view( + &signer, + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!( + output, ARTICLE, + "the original document fits its exact budget" + ); + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + } + + #[test] + fn a_cached_plan_that_expands_past_the_document_budget_passes_through() { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + let http = Arc::new(StubHttpClient::new()); + let signer = factory_with(config_json(TOKEN), ARTICLE.len()); + + let output = view( + &signer, + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!( + output, ARTICLE, + "the original document fits its exact budget" + ); + assert!( + http.recorded_request_methods().is_empty(), + "a cached plan should not call the API" + ); + } + + #[test] + fn render_budget_matches_the_exact_escaped_output_size() { + let article = factory().extract(ARTICLE).expect("should extract article"); + let plan: EmbeddingPlan = + serde_json::from_value(plan_json(MARKER)).expect("should build a valid plan"); + let manifest_url = r#"https://manifest.example/?a=1&b="x""#; + let expected = render(ARTICLE, &article, &plan, Some(manifest_url), usize::MAX) + .expect("unbounded render should succeed"); + assert!( + expected.contains("a=1&b="x"<y>"), + "fixture should exercise every escaped manifest expansion" + ); + + assert_eq!( + render(ARTICLE, &article, &plan, Some(manifest_url), expected.len(),) + .expect("the exact output budget should fit"), + expected + ); + assert_eq!( + render( + ARTICLE, + &article, + &plan, + Some(manifest_url), + expected.len() - 1, + ), + Err(embed::ApplyError::TooLarge), + "one byte below the exact output size must fail before rendering" + ); + } + + #[test] + fn render_orders_equal_head_offset_markers_before_manifest_meta() { + let html = "A"; + let article = article_with_selector(html, Some("head")); + let plan = EmbeddingPlan { + index_unit: "codepoint".to_owned(), + operations: vec![ + api::PlanOperation { + insert_after_index: 0, + marker: "\u{fe00}".to_owned(), + }, + api::PlanOperation { + insert_after_index: 0, + marker: "\u{fe01}".to_owned(), + }, + ], + }; + + assert_eq!( + render( + html, + &article, + &plan, + Some(r#"https://example.com/?a=1&b="x""#), + usize::MAX, + ) + .expect("render should succeed"), + concat!( + "A\u{fe00}\u{fe01}", + r#""#, + "" + ) + ); + } + + #[test] + fn render_preserves_manifest_placement_around_malformed_head_closes() { + let plan = EmbeddingPlan { + index_unit: "codepoint".to_owned(), + operations: vec![api::PlanOperation { + insert_after_index: 0, + marker: "\u{fe00}".to_owned(), + }], + }; + let meta = r#""#; + for (html, expected) in [ + ( + "
A
", + format!("{meta}
A\u{fe00}
"), + ), + ( + "
AB
", + format!("
A\u{fe00}{meta}B
"), + ), + ] { + let article = article_with_selector(html, None); + assert_eq!( + render( + html, + &article, + &plan, + Some("https://manifest.example.com/m"), + usize::MAX, + ) + .expect("render should succeed"), + expected + ); + } + } + + #[test] + fn render_omits_manifest_for_an_empty_url_or_missing_head_close() { + let plan = EmbeddingPlan { + index_unit: "codepoint".to_owned(), + operations: vec![api::PlanOperation { + insert_after_index: 0, + marker: "\u{fe00}".to_owned(), + }], + }; + for (html, selector, manifest_url, expected) in [ + ( + "
A
", + None, + Some("https://manifest.example.com/m"), + "
A\u{fe00}
", + ), + ( + "A", + Some("head"), + Some(""), + "A\u{fe00}", + ), + ] { + let article = article_with_selector(html, selector); + assert_eq!( + render(html, &article, &plan, manifest_url, usize::MAX,) + .expect("render should succeed"), + expected + ); + } + } + + #[test] + fn a_cached_plan_signs_without_calling_the_api() { + let kv = Arc::new(MemoryKv::default()); + let first = seed_plan(&kv); + + let http = Arc::new(StubHttpClient::new()); + let second = view( + &factory(), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!( + second, first, + "a cache hit should serve the same signed page" + ); + assert!( + http.recorded_backend_names().is_empty(), + "should make no API call" + ); + } + + #[test] + fn a_cached_skip_passes_through_without_calling_the_api() { + let kv = Arc::new(MemoryKv::default()); + let first_http = Arc::new(StubHttpClient::new()); + first_http.push_response(200, policy_response(POLICY_DIGEST)); + first_http.push_response(403, b"{}".to_vec()); + view(&factory(), &services(first_http, &kv)); + assert_eq!(kv.only_entry().1["kind"], "skip"); + + let http = Arc::new(StubHttpClient::new()); + let output = view( + &factory(), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE); + assert!( + http.recorded_backend_names().is_empty(), + "should make no API call" + ); + } + + #[test] + fn a_delayed_skip_write_cannot_overwrite_a_concurrent_plan() { + let seeded = Arc::new(MemoryKv::default()); + seed_plan(&seeded); + let (plan_key, _) = seeded.only_entry(); + let policy_key = policy_key(); + let kv = Arc::new(MemoryKv { + entries: Mutex::new(HashMap::from([( + policy_key.clone(), + seeded.entry_bytes(&policy_key), + )])), + concurrent_positive_before_skip_write: Mutex::new(Some(( + plan_key, + seeded + .entries() + .into_iter() + .find(|(key, _)| key.starts_with(PLAN_KEY_PREFIX)) + .expect("seeded plan") + .1, + ))), + ..MemoryKv::default() + }); + let failed = Arc::new(StubHttpClient::new()); + failed.push_response(503, Vec::new()); + + assert_eq!( + view( + &factory(), + &services(Arc::clone(&failed) as Arc, &kv), + ), + ARTICLE, + "the failed request should pass through" + ); + assert_eq!(failed.recorded_request_methods(), ["POST"]); + let entries = kv.entries(); + assert!( + entries + .values() + .any(|value| serde_json::from_slice::(value) + .is_ok_and(|entry| entry["kind"] == "plan")), + "the concurrent plan must survive" + ); + assert!( + entries + .values() + .any(|value| serde_json::from_slice::(value) + .is_ok_and(|entry| entry["kind"] == "skip")), + "the delayed failure should remain negatively cached under its own key" + ); + + let no_api = Arc::new(StubHttpClient::new()); + assert_signed(&view( + &factory(), + &services(Arc::clone(&no_api) as Arc, &kv), + )); + assert!(no_api.recorded_request_methods().is_empty()); + } + + #[test] + fn a_delayed_policy_skip_cannot_overwrite_a_concurrent_digest() { + let seeded = Arc::new(MemoryKv::default()); + seed_plan(&seeded); + let policy_key = policy_key(); + let kv = Arc::new(MemoryKv { + concurrent_positive_before_skip_write: Mutex::new(Some(( + policy_key.clone(), + seeded.entry_bytes(&policy_key), + ))), + ..MemoryKv::default() + }); + let failed = Arc::new(StubHttpClient::new()); + failed.push_response(503, Vec::new()); + + assert_eq!( + view( + &factory(), + &services(Arc::clone(&failed) as Arc, &kv), + ), + ARTICLE, + ); + assert_eq!(failed.recorded_request_methods(), ["GET"]); + let entries = kv.entries(); + assert!( + entries + .values() + .any(|value| serde_json::from_slice::(value) + .is_ok_and(|entry| entry["kind"] == "digest")), + "the concurrent policy digest must survive" + ); + assert!( + entries + .values() + .any(|value| serde_json::from_slice::(value) + .is_ok_and(|entry| entry["kind"] == "skip")), + "the delayed policy failure should remain under its own key" + ); + + let sign = Arc::new(StubHttpClient::new()); + sign.push_response(200, sign_response(MARKER)); + assert_signed(&view( + &factory(), + &services(Arc::clone(&sign) as Arc, &kv), + )); + assert_eq!(sign.recorded_request_methods(), ["POST"]); + } + + #[test] + fn sign_failures_pass_through_and_write_a_skip() { + let oversized = { + let mut body = sign_response(MARKER); + body.resize(api::MAX_RESPONSE_BYTES + 2, b' '); + body + }; + let quota = serde_json::to_vec(&json!({ "success": false, "error": "quota_exceeded" })) + .expect("should serialize"); + let insecure_manifest = serde_json::to_vec(&json!({ + "success": true, + "policy_digest": POLICY_DIGEST, + "embedding_plan": plan_json(MARKER), + "manifest_url": "javascript:alert(1)", + })) + .expect("should serialize"); + let cases: Vec = vec![ + ("403", Some((403, b"{}".to_vec()))), + ("422", Some((422, b"{}".to_vec()))), + ("429", Some((429, b"{}".to_vec()))), + ("5xx", Some((502, Vec::new()))), + ("transport error or first-byte timeout", None), + ("oversized body", Some((200, oversized))), + ("malformed body", Some((200, b"not json".to_vec()))), + ( + "invalid marker", + Some((200, sign_response(""))), + ), + ("quota", Some((200, quota))), + ("non-https manifest url", Some((200, insecure_manifest))), + ( + "null plan", + Some(( + 200, + serde_json::to_vec(&json!({ + "success": true, "policy_digest": POLICY_DIGEST, "embedding_plan": null + })) + .expect("JSON"), + )), + ), + ]; + + for (case, response) in cases { + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + if let Some((status, body)) = response { + http.push_response(status, body); + } + let kv = Arc::new(MemoryKv::default()); + + let output = view( + &factory(), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!( + output, ARTICLE, + "{case}: should pass the page through unchanged" + ); + assert_eq!( + http.recorded_backend_names().len(), + 2, + "{case}: one policy call and one sign call" + ); + assert_eq!( + kv.only_entry().1["kind"], + "skip", + "{case}: should cache a skip" + ); + } + } + + #[test] + fn a_tampered_cached_plan_is_evicted_then_the_next_view_recovers() { + for (field, value) in [ + ("/plan/operations/0/marker", ""), + ("/manifest_url", "http://manifest.example.com/doc-1"), + ] { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + let (key, mut entry) = kv.only_entry(); + *entry + .pointer_mut(field) + .expect("seeded entry should have the field") = json!(value); + kv.insert(&key, &entry); + + let http = Arc::new(StubHttpClient::new()); + let services = services(Arc::clone(&http) as Arc, &kv); + assert_eq!( + view(&factory(), &services), + ARTICLE, + "{field}: should pass through" + ); + assert!( + http.recorded_backend_names().is_empty(), + "{field}: should make no API call on the failure view" + ); + assert!( + kv.deletes.lock().expect("lock deletes").contains(&key), + "{field}: the invalid plan should be evicted" + ); + + http.push_response(200, sign_response(MARKER)); + assert_signed(&view(&factory(), &services)); + assert_eq!(http.recorded_request_methods(), ["POST"], "{field}"); + } + } + + #[test] + fn malformed_ttl_less_plan_is_evicted_then_the_next_view_recovers() { + let kv = Arc::new(MemoryKv { + ttl_unsupported: true, + ..MemoryKv::default() + }); + seed_plan(&kv); + let (key, _) = kv.only_entry(); + kv.insert_raw(&key, Bytes::from_static(b"not json")); + let http = Arc::new(StubHttpClient::new()); + let services = services(Arc::clone(&http) as Arc, &kv); + + assert_eq!(view(&factory(), &services), ARTICLE); + assert!(http.recorded_request_methods().is_empty()); + assert!( + kv.deletes.lock().expect("lock deletes").contains(&key), + "malformed entries must be deleted even when their ttl_less flag cannot be decoded" + ); + + http.push_response(200, sign_response(MARKER)); + assert_signed(&view(&factory(), &services)); + assert_eq!(http.recorded_request_methods(), ["POST"]); + } + + #[test] + fn representation_alignment_failure_does_not_evict_a_valid_cached_plan() { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + let (key, _) = kv.only_entry(); + let signer = factory(); + let mut article = signer.extract(ARTICLE).expect("should extract article"); + article.boundary.end = ARTICLE.len() + 1; + let http = Arc::new(StubHttpClient::new()); + let services = services(http, &kv); + + let result = futures::executor::block_on(async { + let mut work = Deadline::Unbounded; + signer + .resolve( + &request(), + &services, + ARTICLE, + &article, + POLICY_DIGEST, + &mut work, + ) + .await + }); + + assert_eq!( + result.expect_err("alignment should fail"), + Reason::AlignmentFailed + ); + assert!( + kv.entries().contains_key(&key), + "valid plan should remain cached" + ); + assert!( + !kv.deletes.lock().expect("lock deletes").contains(&key), + "representation-specific alignment must not evict a reusable plan" + ); + } + + #[test] + fn extraction_cap_counts_the_inter_fragment_separator() { + let leading = "a".repeat(api::MAX_TEXT_SCALARS - 1); + let html = format!( + "

{leading}

{}

", + "&".repeat(100_000) + ); + let article = factory().extract(&html).expect("should extract article"); + + assert_eq!(article.scalars, api::MAX_TEXT_SCALARS); + assert_eq!(article.text, format!("{leading} ")); + } + + #[test] + fn underlength_article_maps_a_valid_api_range_plan_to_alignment_failed() { + let html = "
A
"; + let article = article_with_selector(html, None); + assert_eq!( + html.get(article.boundary.start..article.boundary.end), + Some(html) + ); + let plan = EmbeddingPlan { + index_unit: "codepoint".to_owned(), + operations: vec![api::PlanOperation { + insert_after_index: 1, + marker: MARKER.to_owned(), + }], + }; + assert!( + api::validate_plan(&plan, None, api::MAX_TEXT_SCALARS).is_ok(), + "the plan index is valid within the signing API range" + ); + + let reason = render(html, &article, &plan, None, usize::MAX) + .map_err(render_reason) + .expect_err("the article has no canonical scalar at index 1"); + assert_eq!(reason, Reason::AlignmentFailed); + } + + #[test] + fn a_changed_credential_misses_the_cache() { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + + let http = signing_http(); + let output = view( + &factory_with(config_json("cwk_rotated_token"), 5_000_000), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_signed(&output); + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + assert_eq!( + kv.entries().len(), + 4, + "each credential should have its own policy and article entries" + ); + } + + // --- asynchronous resolve and the hold deadline ------------------------- + + #[test] + fn calls_that_are_pending_before_ready_are_awaited_and_sign() { + let timers: [Option>; 3] = [ + None, + Some(Arc::new(StoppedTimer)), + Some(Arc::new(TickTimer::default())), + ]; + for timer in timers { + let label = if timer.is_some() { "timer" } else { "no timer" }; + let kv = Arc::new(MemoryKv { + pending_polls: 2, + ..MemoryKv::default() + }); + let stub = signing_http(); + let http = Arc::new(YieldingHttpClient { + inner: Arc::clone(&stub), + pending_polls: 2, + }); + let mut services = services(http, &kv); + if let Some(timer) = timer { + services = services.with_timer(timer); + } + + let first = view(&factory(), &services); + let second = view(&factory(), &services); + + assert_signed(&first); + assert_eq!( + second, first, + "{label}: the second view should hit the cache" + ); + assert_eq!(stub.recorded_request_methods(), ["GET", "POST"], "{label}"); + assert_eq!(kv.only_entry().1["kind"], "plan", "{label}"); + } + } + + #[test] + fn a_hung_policy_call_times_out_writes_a_skip_and_the_next_view_makes_no_call() { + let kv = Arc::new(MemoryKv::default()); + let hung = Arc::new(HungHttpClient::default()); + + let output = view( + &factory(), + &timed_services(Arc::clone(&hung) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE, "should release the page unchanged"); + assert_eq!(hung.sends.load(Ordering::SeqCst), 1); + let skip = kv.entry(&policy_skip_key()); + assert_eq!(skip["kind"], "skip"); + assert_eq!(skip["reason"], "timeout"); + + let http = Arc::new(StubHttpClient::new()); + let next = view( + &factory(), + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + assert_eq!(next, ARTICLE); + assert!( + http.recorded_backend_names().is_empty(), + "the next view should be a cached skip" + ); + } + + #[test] + fn a_hung_sign_call_or_body_times_out_writes_a_plan_skip_and_the_next_view_makes_no_call() { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + let article_key = kv.only_entry().0; + kv.remove(&article_key); + let hung = Arc::new(HungHttpClient::default()); + let services = timed_services(Arc::clone(&hung) as Arc, &kv); + + for _ in 0..2 { + assert_eq!(view(&factory(), &services), ARTICLE); + } + + assert_eq!( + hung.sends.load(Ordering::SeqCst), + 1, + "one sign call in total" + ); + let (_, skip) = kv.only_entry(); + assert_eq!(skip["kind"], "skip"); + assert_eq!(skip["reason"], "timeout"); + } + + #[test] + fn a_hung_kv_read_times_out_without_calling_the_api() { + let kv = Arc::new(MemoryKv { + hung_reads: true, + ..MemoryKv::default() + }); + let http = Arc::new(StubHttpClient::new()); + + let output = view( + &factory(), + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE); + assert_eq!(kv.reads(), 1); + assert!(http.recorded_backend_names().is_empty()); + } + + #[test] + fn a_plan_write_that_runs_out_of_time_still_returns_the_signed_page() { + let kv = Arc::new(MemoryKv { + hung_writes_to: Some(PLAN_KEY_PREFIX), + ..MemoryKv::default() + }); + let http = signing_http(); + + let output = view( + &factory(), + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + + assert_signed(&output); + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + assert!( + kv.entries() + .keys() + .all(|key| !key.starts_with(PLAN_KEY_PREFIX)), + "the abandoned plan write should store nothing" + ); + } + + #[test] + fn a_policy_digest_write_that_runs_out_of_time_caches_a_policy_skip() { + let kv = Arc::new(MemoryKv { + hung_first_write_to: Some(policy_key()), + ..MemoryKv::default() + }); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + + let output = view( + &factory(), + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE, "should release the page unchanged"); + assert_eq!( + http.recorded_request_methods(), + ["GET"], + "no sign call after the digest write ran out of time" + ); + let skip = kv.entry(&policy_skip_key()); + assert_eq!(skip["kind"], "skip"); + assert_eq!(skip["reason"], "timeout"); + assert!( + kv.entries() + .keys() + .all(|key| !key.starts_with(PLAN_SKIP_KEY_PREFIX)), + "no plan skip under a digest the next view cannot know" + ); + + // A hung client turns any API call into a full hold, so zero sends + // after the positive and negative policy reads shows a cached skip. + let reads = kv.reads(); + let hung = Arc::new(HungHttpClient::default()); + let next = view( + &factory(), + &timed_services(Arc::clone(&hung) as Arc, &kv), + ); + assert_eq!(next, ARTICLE); + assert_eq!(hung.sends.load(Ordering::SeqCst), 0, "no API call"); + assert_eq!(kv.reads() - reads, 2, "positive policy miss, then skip hit"); + } + + #[test] + fn the_core_backstop_lies_beyond_the_module_hold_bound() { + let kv = Arc::new(MemoryKv::default()); + let timer = Arc::new(TickTimer::default()); + let services = services(Arc::new(HungHttpClient::default()), &kv) + .with_timer(Arc::clone(&timer) as Arc); + + assert_eq!(view(&factory(), &services), ARTICLE); + + let max_hold = Duration::from_millis(5_000); + let requested = timer.requested.lock().expect("lock").clone(); + let (backstop, module): (Vec<_>, Vec<_>) = + requested.into_iter().partition(|sleep| *sleep > max_hold); + assert_eq!(backstop.len(), 1, "one core backstop beyond max_hold_ms"); + // The skip-write reserve starts only after the work deadline, so the + // module's own sleeps run back to back. + assert!( + !module.is_empty() && module.iter().sum::() <= max_hold, + "module sleeps {module:?} should fit within max_hold_ms" + ); + assert_eq!( + kv.entry(&policy_skip_key())["reason"], + "timeout", + "the module deadline, not the backstop, should release the page" + ); + } + + // --- expiry, write mode and key namespaces ------------------------------ + + #[test] + fn expired_entries_are_misses() { + // (entry to expire, API calls the next view makes) + let cases: [(&str, &[&str]); 4] = [ + ("policy digest", &["GET"]), + ("policy skip", &["GET"]), + ("plan", &["POST"]), + ("plan skip", &["POST"]), + ]; + for (case, expected_calls) in cases { + let kv = Arc::new(MemoryKv::default()); + seed_plan(&kv); + let (plan_key, _) = kv.only_entry(); + let key = if case.starts_with("policy") { + policy_key() + } else { + plan_key + }; + if case.ends_with("skip") { + kv.insert( + &key, + &json!({"kind": "skip", "reason": "server_error", "expires_at": expired()}), + ); + } else { + set_expiry(&kv, &key, expired()); + } + let http = Arc::new(StubHttpClient::new()); + if case.starts_with("policy") { + http.push_response(200, policy_response(POLICY_DIGEST)); + } else { + http.push_response(200, sign_response(MARKER)); + } + + let output = view( + &factory(), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_signed(&output); + assert_eq!(http.recorded_request_methods(), expected_calls, "{case}"); + assert!( + kv.entry(&key)["expires_at"].as_u64() > Some(unix_now()), + "{case}: should be replaced by a fresh entry" + ); + } + } + + #[test] + fn an_expired_plan_does_not_block_a_skip_write() { + let seeded = Arc::new(MemoryKv::default()); + seed_plan(&seeded); + let kv = Arc::new(MemoryKv { + entries: Mutex::new(seeded.entries()), + hidden_reads: AtomicUsize::new(1), + ..MemoryKv::default() + }); + let (plan_key, _) = kv.only_entry(); + set_expiry(&kv, &plan_key, expired()); + let failed = Arc::new(StubHttpClient::new()); + failed.push_response(503, Vec::new()); + + assert_eq!( + view( + &factory(), + &services(Arc::clone(&failed) as Arc, &kv), + ), + ARTICLE + ); + assert_eq!(failed.recorded_request_methods(), ["POST"]); + + let no_api = Arc::new(StubHttpClient::new()); + assert_eq!( + view( + &factory(), + &services(Arc::clone(&no_api) as Arc, &kv), + ), + ARTICLE + ); + assert!( + no_api.recorded_request_methods().is_empty(), + "the fresh skip should suppress a second signing call" + ); + } + + #[test] + fn a_store_without_ttl_writes_switches_once_and_expires_by_value() { + let kv = Arc::new(MemoryKv { + ttl_unsupported: true, + ..MemoryKv::default() + }); + let factory = factory(); + let http = signing_http(); + let services = services(Arc::clone(&http) as Arc, &kv); + + let first = view(&factory, &services); + + assert_signed(&first); + assert_eq!( + kv.ttl_writes.load(Ordering::SeqCst), + 1, + "only the first write should try a TTL" + ); + let (plan_key, plan) = kv.only_entry(); + assert_eq!(plan["kind"], "plan"); + assert!(plan["expires_at"].as_u64() > Some(unix_now())); + assert_eq!(kv.entry(&policy_key())["kind"], "digest"); + assert_eq!(view(&factory, &services), first, "should hit the cache"); + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + + // Expired entries are misses and are deleted when read. + set_expiry(&kv, &plan_key, expired()); + http.push_response(200, sign_response(MARKER)); + assert_signed(&view(&factory, &services)); + assert_eq!(*kv.deletes.lock().expect("lock"), [plan_key.as_str()]); + assert_eq!( + kv.entry(&plan_key)["kind"], + "plan", + "should store a new plan" + ); + } + + #[test] + fn a_fresh_instance_deletes_expired_entries_written_without_a_ttl() { + // Spin instantiates the module per request, so the instance that reads + // an expired entry has not written anything yet. + let kv = Arc::new(MemoryKv { + ttl_unsupported: true, + ..MemoryKv::default() + }); + view(&factory(), &services(signing_http(), &kv)); + set_expiry(&kv, &policy_key(), expired()); + let http = Arc::new(StubHttpClient::new()); + http.push_response(503, Vec::new()); + + let output = view( + &factory(), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE); + assert_eq!(http.recorded_request_methods(), ["GET"]); + assert_eq!(*kv.deletes.lock().expect("lock"), [policy_key()]); + } + + #[test] + fn expired_entries_are_not_deleted_on_a_store_with_ttls() { + let kv = Arc::new(MemoryKv::default()); + let http = signing_http(); + let services = services(Arc::clone(&http) as Arc, &kv); + view(&factory(), &services); + let (plan_key, _) = kv.only_entry(); + set_expiry(&kv, &plan_key, expired()); + http.push_response(200, sign_response(MARKER)); + + assert_signed(&view(&factory(), &services)); + + assert!(kv.deletes.lock().expect("lock").is_empty()); + } + + #[test] + fn entries_under_the_previous_key_versions_are_never_read() { + let current = Arc::new(MemoryKv::default()); + seed_plan(¤t); + // The same plan and policy, as the previous release stored them. + let kv = Arc::new(MemoryKv::default()); + for (key, value) in current.entries() { + let mut entry: serde_json::Value = + serde_json::from_slice(&value).expect("should store JSON"); + entry.as_object_mut().expect("object").remove("expires_at"); + let old_key = key + .replace("encypher:plan:v4:", "encypher:v3:") + .replace("encypher:policy:v3:", "encypher:policy:v2:"); + kv.insert(&old_key, &entry); + } + let old_entries = kv.entries(); + let http = signing_http(); + + let output = view( + &factory(), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_signed(&output); + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + let entries = kv.entries(); + assert_eq!(entries.len(), old_entries.len() * 2); + for (key, value) in old_entries { + assert_eq!(entries[&key], value, "{key} should be left untouched"); + } + } + + // --- KV failures -------------------------------------------------------- + + #[test] + fn kv_read_errors_pass_through_without_calling_the_api() { + for kv in [ + MemoryKv { + fail_reads: true, + ..MemoryKv::default() + }, + MemoryKv { + unavailable: true, + ..MemoryKv::default() + }, + ] { + let kv = Arc::new(kv); + let http = Arc::new(StubHttpClient::new()); + + let output = view( + &factory(), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE); + assert_eq!(kv.reads(), 1); + assert!( + http.recorded_backend_names().is_empty(), + "should make no API call" + ); + } + } + + // --- gates and skips ---------------------------------------------------- + + #[test] + fn gated_documents_pass_through_with_no_kv_or_api_io() { + let signer = factory(); + let cases: Vec<(GateCase<'_>, Gate)> = vec![ + ( + ("method", &signer, |r| r.method = Method::POST), + Gate::Method, + ), + ( + ("status", &signer, |r| r.response_status = 404), + Gate::Status, + ), + ( + ("origin private", &signer, |r| { + r.origin_response_is_private = true; + }), + Gate::OriginPrivate, + ), + ( + ("host mismatch", &signer, |r| { + r.page_url = "https://other.example.com/news/harbor-vote".to_owned(); + }), + Gate::HostMismatch, + ), + ( + ("excluded path", &signer, |r| { + r.page_url = "https://publisher.example.com/WP-Admin/post".to_owned(); + }), + Gate::ExcludedPath, + ), + ( + ("excluded path under an addressing prefix", &signer, |r| { + r.page_url = + "https://publisher.example.com/www.site.example/wp-admin/post".to_owned(); + r.removed_prefix = "/www.site.example".to_owned(); + }), + Gate::ExcludedPath, + ), + ( + ("asset suffix", &signer, |r| { + r.page_url = "https://publisher.example.com/feed.XML".to_owned(); + }), + Gate::ExcludedPath, + ), + ( + ("invalid page url", &signer, |r| { + r.page_url = "not a url".to_owned(); + }), + Gate::InvalidPageUrl, + ), + ]; + + for ((case, signer, mutate), gate) in cases { + let http = Arc::new(StubHttpClient::new()); + let kv = Arc::new(MemoryKv::default()); + let services = services(Arc::clone(&http) as Arc, &kv); + let mut request = request(); + mutate(&mut request); + + assert_eq!( + signer.admit(request.clone()), + Err(gate), + "{case}: should be refused by its gate" + ); + let output = run(signer, Some(request), &services, ARTICLE); + + assert_eq!(output, ARTICLE, "{case}: should pass through"); + assert_eq!(kv.reads(), 0, "{case}: should read no KV"); + assert!( + http.recorded_backend_names().is_empty(), + "{case}: no API call" + ); + } + + let kv = Arc::new(MemoryKv::default()); + assert_eq!( + run( + &factory(), + None, + &services(Arc::new(StubHttpClient::new()), &kv), + ARTICLE + ), + ARTICLE, + "nothing prepared" + ); + } + + #[test] + fn a_bare_domain_page_is_signed() { + let kv = Arc::new(MemoryKv::default()); + let mut request = request(); + request.page_url = "https://publisher.example.com/news/harbor-vote".to_owned(); + + let output = run( + &factory(), + Some(request), + &services(signing_http(), &kv), + ARTICLE, + ); + + assert_signed(&output); + } + + #[test] + fn an_oversized_document_is_flushed_and_passed_through() { + let http = Arc::new(StubHttpClient::new()); + let kv = Arc::new(MemoryKv::default()); + + let output = view( + &factory_with(config_json(TOKEN), 64), + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE); + assert_eq!(kv.reads(), 0); + assert!(http.recorded_backend_names().is_empty()); + } + + #[test] + fn already_signed_or_short_pages_are_not_sent() { + let magic = "\u{E01DF}\u{E01E0}\u{E01E1}\u{E01E2}"; + let marked = ARTICLE.replace("The harbor council", &format!("The harbor{magic} council")); + let with_meta = ARTICLE.replace( + "", + r#""#, + ); + let short = "

Too short.

"; + + for page in [marked.as_str(), with_meta.as_str(), short] { + let http = Arc::new(StubHttpClient::new()); + let kv = Arc::new(MemoryKv::default()); + + let output = run( + &factory(), + Some(request()), + &services(Arc::clone(&http) as Arc, &kv), + page, + ); + + assert_eq!(output, page); + assert_eq!(kv.reads(), 0, "should read no KV"); + assert!( + http.recorded_backend_names().is_empty(), + "should make no API call" + ); + } + } + + #[test] + fn logs_carry_outcomes_but_never_the_token() { + let kv = Arc::new(MemoryKv::default()); + let Some(logs) = capture_logs(|| { + view(&factory(), &services(signing_http(), &kv)); + view(&factory(), &services(Arc::new(StubHttpClient::new()), &kv)); + let failing = Arc::new(StubHttpClient::new()); + failing.push_response(403, b"{}".to_vec()); + view( + &factory(), + &services(failing, &Arc::new(MemoryKv::default())), + ); + }) else { + // Log lines go to a logger installed elsewhere and cannot be observed here. + return; + }; + + assert!( + logs.iter() + .any(|line| line.ends_with("outcome=signed reason=signed")), + "should log the signing outcome: {logs:?}" + ); + for line in &logs { + assert!(!line.contains(TOKEN), "log line leaks the token: {line}"); + assert!( + !line.contains("encypher:plan:v4:") && !line.contains("encypher:policy:v3:"), + "log line leaks the cache key: {line}" + ); + assert!( + !line.contains("ferry"), + "log line leaks article text: {line}" + ); + } + } + + // --- asynchronous prepared mode: reader delivery ------------------------ + + const SIGNAL_SECRET: &str = "signal-secret-for-tests"; + const CANONICAL_PAGE: &str = "https://publisher.example.com/news/harbor-vote"; + const SIGNALS_URL: &str = "https://prep.publisher.example.com/v1/preparation/signals"; + const CHANGED_ARTICLE: &str = "Harbor Council Votes\ +

Harbor council votes on the ferry schedule

\ +

The harbor council met on Wednesday evening and approved a revised ferry schedule \ + for the coming season.

Residents asked for earlier departures on weekdays.

\ +
Footer
"; + + fn async_factory(reader_trigger: bool) -> Arc { + let mut config = config_json(TOKEN); + config["execution"] = json!("async_prepared"); + config["preparation"] = json!({ + "worker_url": "https://prep.publisher.example.com", + "loopback_url": "https://publisher.example.com", + "deployment_id": "test", + "public_host": "publisher.example.com", + "signal_secret": SIGNAL_SECRET, + "publication_secret": "publication-secret-for-tests", + "reader_trigger": reader_trigger, + "routes": ["/news/"], + }); + factory_with(config, 5_000_000) + } + + /// The identity the worker publishes the article of `html` under, by + /// the one item extraction both sides run. + fn text_identity(factory: &Signer, html: &str) -> (String, usize) { + let article = extract_article( + html, + &factory.config.article_selector, + factory.config.min_text_length, + ) + .expect("should find the article"); + let context = crate::preparation::items::ItemContext { + credential_context: &factory.cache_context, + policy_digest: POLICY_DIGEST, + page_url: CANONICAL_PAGE, + variant: "", + article_selector: &factory.config.article_selector, + min_text_length: factory.config.min_text_length, + max_media_bytes: factory.config.max_media_bytes, + }; + let item = crate::preparation::items::text_item(&context, html, &article); + (item.identity, item.scalars) + } + + fn publish_text(kv: &MemoryKv, identity: &str, scalars: usize) { + kv.insert( + &crate::preparation::protocol::result_key(identity), + &json!({ + "protocol": crate::preparation::protocol::PREPARATION_PROTOCOL, + "identity": identity, + "retirement_epoch": 0, + "result": { + "kind": "text", + "plan": plan_json(MARKER), + "manifest_url": MANIFEST_URL, + "scalars": scalars, + }, + }), + ); + } + + fn retire_text(kv: &MemoryKv, identity: &str) { + kv.insert( + &crate::preparation::protocol::marker_key(identity), + &json!({"epoch": 1}), + ); + } + + /// The policy cached as a delivery instance holds it. + fn cache_policy(factory: &Signer, kv: &MemoryKv) { + kv.insert( + &factory.policy_key, + &json!({ + "kind": "digest", + "policy_digest": POLICY_DIGEST, + "expires_at": unix_now() + 3600, + }), + ); + } + + fn assert_no_signing_call(http: &StubHttpClient) { + for uri in http.recorded_request_uris() { + assert!( + !uri.contains("/cdn/sign") && !uri.contains("/media/") && !uri.contains("/hash/"), + "a reader should never call the signer: {uri}" + ); + } + } + + /// The hint the reader sent, decoded, with its headers. + fn sent_hint( + http: &StubHttpClient, + ) -> ( + crate::preparation::protocol::SignalRequest, + Vec<(String, String)>, + ) { + let uris = http.recorded_request_uris(); + let index = uris + .iter() + .position(|uri| uri == SIGNALS_URL) + .expect("should send a hint"); + let body = &http.recorded_request_bodies()[index]; + ( + serde_json::from_slice(body).expect("should send a signal request"), + http.recorded_request_headers()[index].clone(), + ) + } + + #[test] + fn async_reader_applies_a_published_result_without_any_signing_call() { + let factory = async_factory(true); + let kv = Arc::new(MemoryKv::default()); + let (identity, scalars) = text_identity(&factory, ARTICLE); + publish_text(&kv, &identity, scalars); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + + let output = view( + &factory, + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + + assert_signed(&output); + assert_eq!( + http.recorded_request_uris().len(), + 1, + "only the policy authority lookup should leave the instance" + ); + assert!(http.recorded_request_uris()[0].contains("/trusted-server/policy")); + assert_no_signing_call(&http); + } + + #[test] + fn async_reader_does_not_apply_a_result_from_the_old_text_cache_contract() { + use crate::preparation::protocol::{TEXT_PROJECTION_VERSION, domain_digest}; + + let factory = async_factory(true); + let article = extract_article( + ARTICLE, + &factory.config.article_selector, + factory.config.min_text_length, + ) + .expect("should find the article"); + let title = + text::title_from_range(ARTICLE, article.title_range).expect("should find the title"); + let text_hash = crate::media_api::sha256_hex(article.text.as_bytes()); + let old_identity = domain_digest( + "encypher-preparation/1:text", + &[ + factory.cache_context.as_bytes(), + POLICY_DIGEST.as_bytes(), + CANONICAL_PAGE.as_bytes(), + b"", + TEXT_PROJECTION_VERSION.as_bytes(), + text_hash.as_bytes(), + b"1", + title.as_bytes(), + article.boundary.selector.as_bytes(), + ], + ); + for reader_trigger in [false, true] { + let factory = async_factory(reader_trigger); + let kv = Arc::new(MemoryKv::default()); + publish_text(&kv, &old_identity, article.scalars); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + if reader_trigger { + http.push_response(202, b"{}".to_vec()); + } + let output = view( + &factory, + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + assert_eq!( + output, ARTICLE, + "an old text result must leave the current delivery unsigned" + ); + assert_no_signing_call(&http); + if reader_trigger { + let (hint, _) = sent_hint(&http); + assert_eq!(hint.items[0].identity, text_identity(&factory, ARTICLE).0); + assert_ne!(hint.items[0].identity, old_identity); + } else { + assert_eq!(http.recorded_request_uris().len(), 1); + } + } + } + + #[test] + fn async_reader_miss_delivers_the_current_bytes_unsigned_and_hints_within_its_budget() { + let factory = async_factory(true); + let kv = Arc::new(MemoryKv::default()); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + http.push_response(202, b"{}".to_vec()); + + let output = view( + &factory, + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!( + output, ARTICLE, + "should deliver the current version unsigned" + ); + assert_no_signing_call(&http); + let (hint, headers) = sent_hint(&http); + let (identity, _) = text_identity(&factory, ARTICLE); + assert_eq!(hint.deployment_id, "test"); + assert_eq!(hint.target.path, "/news/harbor-vote"); + assert_eq!(hint.target.variant, ""); + assert_eq!( + hint.items, + [crate::preparation::protocol::ObservedItem { + kind: crate::preparation::protocol::ItemKind::Text, + identity, + }], + "should name the identity of exactly the bytes delivered" + ); + assert!( + headers.iter().any(|(name, value)| { + name == crate::preparation::protocol::TOKEN_HEADER && value == SIGNAL_SECRET + }), + "should authenticate with the reader-signal scope only" + ); + assert!( + !headers + .iter() + .any(|(name, value)| value.contains(TOKEN) || name == "cookie"), + "should carry no worker credential or reader state" + ); + } + + #[test] + fn async_reader_never_applies_a_result_published_for_other_bytes() { + let factory = async_factory(true); + let kv = Arc::new(MemoryKv::default()); + let (old_identity, scalars) = text_identity(&factory, ARTICLE); + publish_text(&kv, &old_identity, scalars); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + http.push_response(202, b"{}".to_vec()); + + let output = run( + &factory, + Some(request()), + &timed_services(Arc::clone(&http) as Arc, &kv), + CHANGED_ARTICLE, + ); + + assert_eq!( + output, CHANGED_ARTICLE, + "the changed article should go unsigned" + ); + let (hint, _) = sent_hint(&http); + let (new_identity, _) = text_identity(&factory, CHANGED_ARTICLE); + assert_ne!(new_identity, old_identity); + assert_eq!( + hint.items[0].identity, new_identity, + "should hint the changed version" + ); + assert_no_signing_call(&http); + } + + #[test] + fn async_reader_refuses_a_retired_or_unreadable_marker_and_does_not_hint() { + let factory = async_factory(true); + let (identity, scalars) = text_identity(&factory, ARTICLE); + + let retired = Arc::new(MemoryKv::default()); + publish_text(&retired, &identity, scalars); + retire_text(&retired, &identity); + let unreadable = Arc::new(MemoryKv { + fail_reads_of: Some("encypher:prep:v1:retired:"), + ..MemoryKv::default() + }); + publish_text(&unreadable, &identity, scalars); + + for (name, kv) in [("retired", retired), ("unreadable", unreadable)] { + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + let output = view( + &factory, + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + assert_eq!( + output, ARTICLE, + "{name}: the published result should not be applied" + ); + assert!( + !http + .recorded_request_uris() + .iter() + .any(|uri| uri == SIGNALS_URL), + "{name}: a refused result is not an unprepared item" + ); + assert_no_signing_call(&http); + } + } + + #[test] + fn async_reader_hint_to_an_unresponsive_worker_is_bounded_by_its_budget() { + let factory = async_factory(true); + let kv = Arc::new(MemoryKv::default()); + cache_policy(&factory, &kv); + let http = Arc::new(HungHttpClient::default()); + + let output = view( + &factory, + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE, "the reader should get the current version"); + assert_eq!( + http.sends.load(Ordering::SeqCst), + 1, + "the one hint should be abandoned at its budget, never retried" + ); + } + + #[test] + fn async_reader_without_a_timer_hands_the_hint_over_after_the_response() { + let factory = async_factory(true); + let kv = Arc::new(MemoryKv::default()); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + declare_platform_after_response_handoff(); + drop(take_after_response_requests()); + + let output = view( + &factory, + &services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE); + assert!( + !http + .recorded_request_uris() + .iter() + .any(|uri| uri == SIGNALS_URL), + "nothing should be sent while the reader waits" + ); + let handed = take_after_response_requests(); + assert_eq!(handed.len(), 1, "should hand one hint over"); + assert_eq!(handed[0].request.uri().to_string(), SIGNALS_URL); + let hint: crate::preparation::protocol::SignalRequest = + serde_json::from_slice(handed[0].request.body()).expect("should be a signal"); + assert_eq!(hint.items[0].identity, text_identity(&factory, ARTICLE).0); + assert_no_signing_call(&http); + } + + #[test] + fn async_reader_without_reader_trigger_never_hints() { + let factory = async_factory(false); + let kv = Arc::new(MemoryKv::default()); + let http = Arc::new(StubHttpClient::new()); + http.push_response(200, policy_response(POLICY_DIGEST)); + + let output = view( + &factory, + &timed_services(Arc::clone(&http) as Arc, &kv), + ); + + assert_eq!(output, ARTICLE); + assert_eq!( + http.recorded_request_uris().len(), + 1, + "only the policy lookup" + ); + assert_no_signing_call(&http); + } +} diff --git a/crates/provenance/encypher/src/processor/media.rs b/crates/provenance/encypher/src/processor/media.rs new file mode 100644 index 0000000000..eba05b7e2f --- /dev/null +++ b/crates/provenance/encypher/src/processor/media.rs @@ -0,0 +1,950 @@ +//! Media signing through the Encypher media endpoint. +//! +//! One source's exact bytes are sent with the server policy the edge holds, +//! and the answer is kept only after [`media_api::verify_signed_response`] +//! accepts it. Results are cached in the runtime KV store under a namespace +//! that binds the credential context, the policy digest, the delivery +//! address, the source reference, its declared and verified type, its bytes' +//! digest and whether a `Link` can be delivered, so a changed policy, +//! credential or source never reuses another's signature. The replay key the +//! endpoint requires is derived from the same namespace, so a repeated call +//! is answered with the identical bytes. +//! +//! Two callers use it: core's binary delivery seam, through [`EncypherMedia`], +//! and the page and stylesheet processors for embedded `data:` URIs and +//! inline SVG, through [`Signer::sign_embedded`]. Core keeps the signed +//! representations it delivers under [`MediaSigner::namespace`], which the +//! module answers only after a policy check no older than +//! `policy_ttl_seconds`, so a revoked credential or changed policy stops +//! every kept copy from being served. + +use std::sync::Arc; +use std::time::Duration; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use bytes::Bytes; +use edgezero_core::body::Body as EdgeBody; +use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, StatusCode, header}; +use serde::{Deserialize, Serialize}; +use trusted_server_core::integrations::IntegrationRequestState; +use trusted_server_core::media_signing::{ + LargeLookup, LargeLookupJob, LargeMediaJob, LargeMediaSigning, MediaJob, MediaNamespacing, + MediaSigner, MediaSigning, MediaSigningFailure, SignedMedia as DeliveredMedia, SnapshotFacts, +}; +use trusted_server_core::middleware::ResponseHeader; +use trusted_server_core::platform::RuntimeServices; + +use super::{ + Deadline, Expiring, PageRequest, Reason, SKIP_WRITE_RESERVE, Signer, Surface, + WORKER_TOKEN_HEADER, is_excluded_prefix, unix_now, +}; +use crate::MODULE; +use crate::data_uri::DataUri; +use crate::embedded::{self, Scan}; +use crate::media_api::{ + self, Disposition, Expected, MediaMetadata, ResponseError, SignedMedia, SourceMetadata, + canonical_media_type, safe_source_reference, sha256_hex, valid_origin_host, +}; +use crate::svg_equivalence; + +/// Verified signed media. Entries carry their own `expires_at`. +const MEDIA_KEY_PREFIX: &str = "encypher:media:v1"; +/// Media failures, separate so a delayed failure cannot replace a result. +const MEDIA_SKIP_KEY_PREFIX: &str = "encypher:media-skip:v1"; +/// A fresh replay-key salt, written when the endpoint no longer holds or +/// refuses the result for the previous key. +const MEDIA_REPLAY_KEY_PREFIX: &str = "encypher:media-replay:v1"; +const SVG_MEDIA_TYPE: &str = "image/svg+xml"; + +/// The media types the signer signs: images, audio, video and fonts, +/// whatever the path they are delivered on. Whether a format is signable is +/// decided by its verified bytes at the endpoint, which answers an +/// unsupported one `422`, delivered unsigned. +const SIGNABLE_TYPE_PREFIXES: &[&str] = &["image/", "audio/", "video/", "font/"]; + +/// The media type of one segment of a fragmented ISO BMFF stream, as ISO/IEC +/// 14496-12 registers it. A segment's provenance is bound through its +/// stream's init segment, as with C2PA verifiable segment info, so it is +/// never signed alone and reaches the reader as the origin sent it. +const MEDIA_SEGMENT_TYPE: &str = "video/iso.segment"; + +const INSTANCE_ID_HEADER: HeaderName = HeaderName::from_static("x-encypher-manifest-instance-id"); +const DOCUMENT_ID_HEADER: HeaderName = HeaderName::from_static("x-encypher-document-id"); +const VERIFICATION_URL_HEADER: HeaderName = HeaderName::from_static("x-encypher-verification-url"); + +/// One media source to sign, as it is delivered. +struct MediaSource<'a> { + /// The reader-facing address the media is delivered on or in. + page_url: &'a str, + /// A safe canonical address of the source, or none. + reference: Option<&'a str>, + /// The hostname the source was fetched from, or none for media embedded + /// in a document. + origin_host: Option<&'a str>, + /// The declared type, sent as a hint. + mime: &'a str, + /// The bare type the signed bytes must keep. + media_type: &'a str, + bytes: &'a [u8], + sha256: &'a str, + /// Whether every response of the representation can carry a `Link`. + link_deliverable: bool, + /// The bytes are the platform image optimizer's answer, so the endpoint + /// must refuse them unless they carry an intact embedded manifest, the + /// optimizer's own record of its input and what it did. + require_embedded_manifest: bool, +} + +/// A cached media result. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +enum MediaEntry { + /// The verified answer: its base64 bytes and its signing headers, which + /// are verified again on every read. + Signed { + bytes: String, + headers: Vec<(String, String)>, + expires_at: u64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + ttl_less: bool, + }, + Skip { + reason: Reason, + expires_at: u64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + ttl_less: bool, + }, + Replay { + salt: String, + expires_at: u64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + ttl_less: bool, + }, +} + +impl Expiring for MediaEntry { + fn expires_at(&self) -> u64 { + match self { + Self::Signed { expires_at, .. } + | Self::Skip { expires_at, .. } + | Self::Replay { expires_at, .. } => *expires_at, + } + } + + fn ttl_less(&self) -> bool { + match self { + Self::Signed { ttl_less, .. } + | Self::Skip { ttl_less, .. } + | Self::Replay { ttl_less, .. } => *ttl_less, + } + } + + fn mark_ttl_less(&mut self) { + match self { + Self::Signed { ttl_less, .. } + | Self::Skip { ttl_less, .. } + | Self::Replay { ttl_less, .. } => *ttl_less = true, + } + } +} + +impl MediaEntry { + fn signed(signed: &SignedMedia, headers: &HeaderMap, expires_at: u64) -> Self { + let headers = media_api::SIGNING_HEADERS + .iter() + .filter_map(|name| { + let value = headers.get(*name)?.to_str().ok()?; + Some(((*name).to_owned(), value.to_owned())) + }) + .collect(); + Self::Signed { + bytes: STANDARD.encode(&signed.bytes), + headers, + expires_at, + ttl_less: false, + } + } +} + +/// A cached answer, verified as if it had just arrived. +fn verify_cached( + bytes: &str, + headers: &[(String, String)], + expected: &Expected<'_>, +) -> Result { + let body = STANDARD + .decode(bytes) + .map_err(|_| Reason::CachedEntryInvalid)?; + let mut map = HeaderMap::with_capacity(headers.len()); + for (name, value) in headers { + let name = + HeaderName::from_bytes(name.as_bytes()).map_err(|_| Reason::CachedEntryInvalid)?; + let value = HeaderValue::from_str(value).map_err(|_| Reason::CachedEntryInvalid)?; + map.append(name, value); + } + media_api::verify_signed_response(&map, Bytes::from(body), expected) + .map_err(|_| Reason::CachedEntryInvalid) +} + +fn response_reason(error: ResponseError) -> Reason { + match error { + ResponseError::ContentType => Reason::SignedTypeMismatch, + ResponseError::PolicyDigest => Reason::PolicyDigestMismatch, + ResponseError::SignedDigest => Reason::SignedDigestMismatch, + ResponseError::Malformed => Reason::InvalidSignedResponse, + ResponseError::LinkUndeliverable => Reason::ManifestUndeliverable, + ResponseError::Unsigned => Reason::UnsignedResponse, + } +} + +/// The reason for an error answer, by its status and failure code. +fn status_reason(status: StatusCode, code: Option<&str>) -> Reason { + match status.as_u16() { + 403 | 404 => Reason::Forbidden, + 409 if code == Some("SIGNING_IDEMPOTENCY_CONFLICT") => Reason::IdempotencyConflict, + 409 => Reason::PolicyChanged, + 410 => Reason::ResultPurged, + 413 => Reason::PayloadTooLarge, + 422 => Reason::Unprocessable, + 429 => Reason::RateLimited, + 500..=599 => Reason::ServerError, + _ => Reason::UnexpectedStatus, + } +} + +fn log_media(signed: bool, reason: Reason) { + log::info!( + "encypher: media outcome={} reason={}", + if signed { "signed" } else { "unsigned" }, + reason.code() + ); +} + +/// Reports every media item of `scan` unsigned for `reason`. +pub(super) fn log_unsigned(scan: &Scan, reason: Reason) { + for refused in &scan.refused { + log_media(false, *refused); + } + for _ in 0..scan.found.len() + scan.svgs.len() { + log_media(false, reason); + } +} + +/// The document an embedded media pass works for. +pub(super) struct EmbeddedJob<'a> { + pub(super) request: &'a PageRequest, + pub(super) surface: Surface, + pub(super) policy_digest: &'a str, +} + +/// One item's outcome in an embedded pass: signed, from the cache or fresh, +/// or unsigned with why. +type Outcome = Result; + +impl Signer { + fn max_media_bytes(&self) -> usize { + self.config.max_media_bytes + } + + /// The cache namespace of `source` under `policy_digest`: every input the + /// signed result depends on. + fn media_namespace(&self, source: &MediaSource<'_>, policy_digest: &str) -> String { + sha256_hex( + format!( + "{}\n{policy_digest}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + self.cache_context, + source.page_url, + source.reference.unwrap_or_default(), + source.origin_host.unwrap_or_default(), + source.mime, + source.media_type, + source.sha256, + source.link_deliverable, + source.require_embedded_manifest + ) + .as_bytes(), + ) + } + + /// Sign one source, from the cache when a verified result is held. + /// Returns the result and whether it was signed now or read back. A call + /// to the endpoint is made only while `calls` is below + /// `self.config.max_media_sign_calls_per_document`, and counted there. + async fn sign_media( + &self, + services: &RuntimeServices, + source: &MediaSource<'_>, + policy_digest: &str, + work: &mut Deadline<'_>, + calls: &mut usize, + ) -> Result<(SignedMedia, Reason), Reason> { + if source.bytes.len() > self.max_media_bytes() { + return Err(Reason::MediaTooLarge); + } + // Once the budget is spent no call is made and no skip written, so + // the reserve stays for the document's own writes. + if work.expired() { + return Err(Reason::Timeout); + } + let kv = services.kv_store(); + let namespace = self.media_namespace(source, policy_digest); + let key = format!("{MEDIA_KEY_PREFIX}:{namespace}"); + let skip_key = format!("{MEDIA_SKIP_KEY_PREFIX}:{namespace}"); + let expected = Expected { + policy_digest, + media_type: source.media_type, + unsigned_sha256: source.sha256, + link_deliverable: source.link_deliverable, + }; + let outcome = match self.read::(kv, &key, work).await { + Ok(Some(MediaEntry::Signed { bytes, headers, .. })) => { + return match verify_cached(&bytes, &headers, &expected) { + Ok(signed) => Ok((signed, Reason::CacheHit)), + Err(reason) => { + self.delete_invalid_entry(kv, &key, work).await; + Err(reason) + } + }; + } + Ok(Some(_)) => { + self.delete_invalid_entry(kv, &key, work).await; + return Err(Reason::CachedEntryInvalid); + } + Err(Reason::Timeout) => Err(Reason::Timeout), + Err(reason) => return Err(reason), + Ok(None) => match self.read::(kv, &skip_key, work).await { + Ok(Some(MediaEntry::Skip { .. })) => return Err(Reason::CachedSkip), + Ok(Some(_)) => { + self.delete_invalid_entry(kv, &skip_key, work).await; + return Err(Reason::CachedEntryInvalid); + } + Err(Reason::Timeout) => Err(Reason::Timeout), + Err(reason) => return Err(reason), + Ok(None) if *calls >= self.config.max_media_sign_calls_per_document => { + return Err(Reason::Deferred); + } + Ok(None) => { + *calls += 1; + self.request_media(services, source, policy_digest, &namespace, &expected, work) + .await + } + }, + }; + match outcome { + Ok((signed, headers)) => { + // A result write that runs out of time is abandoned; the media + // is still signed. + let entry = MediaEntry::signed( + &signed, + &headers, + Self::expires_at(self.config.plan_ttl_seconds), + ); + self.write(kv, &key, entry, self.config.plan_ttl_seconds, work) + .await; + Ok((signed, Reason::Signed)) + } + Err(reason @ (Reason::ResultPurged | Reason::IdempotencyConflict)) => { + // The next view asks again under a new replay key; no skip, + // and never a retry within this view. + let entry = MediaEntry::Replay { + salt: format!("{}", unix_now()), + expires_at: Self::expires_at(self.config.plan_ttl_seconds), + ttl_less: false, + }; + let replay_key = format!("{MEDIA_REPLAY_KEY_PREFIX}:{namespace}"); + self.write( + kv, + &replay_key, + entry, + self.config.plan_ttl_seconds, + &mut Self::skip_deadline(services), + ) + .await; + Err(reason) + } + Err(reason) => { + let mut deadline = Self::skip_deadline(services); + if matches!(reason, Reason::PolicyChanged | Reason::PolicyDigestMismatch) { + self.evict_policy(kv, &mut deadline).await; + } + let ttl = self.config.negative_ttl_seconds; + let entry = MediaEntry::Skip { + reason, + expires_at: Self::expires_at(ttl), + ttl_less: false, + }; + self.write(kv, &skip_key, entry, ttl, &mut deadline).await; + Err(reason) + } + } + } + + /// Call the media endpoint once with the source's exact bytes. Every + /// failure maps to a reason code. + async fn request_media( + &self, + services: &RuntimeServices, + source: &MediaSource<'_>, + policy_digest: &str, + namespace: &str, + expected: &Expected<'_>, + work: &mut Deadline<'_>, + ) -> Result<(SignedMedia, HeaderMap), Reason> { + let kv = services.kv_store(); + let replay_key = format!("{MEDIA_REPLAY_KEY_PREFIX}:{namespace}"); + let salt = match self.read::(kv, &replay_key, work).await { + Ok(Some(MediaEntry::Replay { salt, .. })) => salt, + Ok(Some(_)) => { + self.delete_invalid_entry(kv, &replay_key, work).await; + return Err(Reason::CachedEntryInvalid); + } + Ok(None) => String::new(), + Err(reason) => return Err(reason), + }; + let idempotency_key = sha256_hex(format!("{namespace}\n{salt}").as_bytes()); + let metadata = serde_json::to_vec(&MediaMetadata::unmodified( + source.page_url, + policy_digest, + SourceMetadata { + reference: source.reference, + origin_host: source.origin_host, + mime: source.mime, + sha256: source.sha256, + byte_length: source.bytes.len(), + require_embedded_manifest: source.require_embedded_manifest, + }, + source.link_deliverable, + )) + .map_err(|_| Reason::RequestBuild)?; + let multipart = media_api::multipart(&metadata, source.bytes); + if multipart.body.len() > media_api::MAX_REQUEST_BYTES { + return Err(Reason::PayloadTooLarge); + } + let http_request = Request::builder() + .method(Method::POST) + .uri(&self.media_sign_url) + .header(header::CONTENT_TYPE, multipart.content_type) + .header(WORKER_TOKEN_HEADER, self.worker_token()?) + .header(media_api::IDEMPOTENCY_KEY_HEADER, idempotency_key) + .body(EdgeBody::from_bytes(multipart.body)) + .map_err(|_| Reason::RequestBuild)?; + let max_body = source + .bytes + .len() + .saturating_add(media_api::MAX_MANIFEST_OVERHEAD_BYTES); + let response_limit = max_body.saturating_add(1); + let response = self + .send_api(services, http_request, response_limit, work) + .await?; + let status = response.status(); + let headers = response.headers().clone(); + if status != StatusCode::OK { + let code = work + .race( + response + .into_body() + .into_bytes_bounded(media_api::MAX_ERROR_BODY_BYTES), + ) + .await + .and_then(Result::ok) + .and_then(|body| media_api::error_code(&body)); + log::info!( + "encypher: media sign answered status={} code={}", + status.as_u16(), + code.as_deref().unwrap_or("none") + ); + return Err(status_reason(status, code.as_deref())); + } + let body = match work + .race(response.into_body().into_bytes_bounded(response_limit)) + .await + { + None => return Err(Reason::Timeout), + Some(Err(_)) => return Err(Reason::ResponseTooLarge), + Some(Ok(body)) => body, + }; + if body.len() > max_body { + return Err(Reason::ResponseTooLarge); + } + let signed = + media_api::verify_signed_response(&headers, body, expected).map_err(response_reason)?; + Ok((signed, headers)) + } + + /// Sign the editable media embedded in `document`: each `data:` URI + /// found, then each inline SVG last as the equivalent standalone document. + /// The scan excludes entire original provenance-bearing or uncertain SVG + /// ranges before either pass, so child signing cannot invalidate them. + /// Returns the new document, or `None` when nothing in it was signed. + pub(super) async fn sign_embedded( + &self, + services: &RuntimeServices, + job: &EmbeddedJob<'_>, + document: &str, + scan: &Scan, + work: &mut Deadline<'_>, + ) -> Option { + for reason in &scan.refused { + log_media(false, *reason); + } + let mut outcomes: Vec = Vec::new(); + let mut edits: Vec<(usize, usize, String)> = Vec::new(); + let mut signed_sources: Vec<(String, String, Result)> = Vec::new(); + let mut calls = 0_usize; + for found in &scan.found { + let Some(uri) = document + .get(found.start..found.end) + .and_then(DataUri::parse) + else { + continue; + }; + let Ok(bytes) = uri.decode() else { + outcomes.push(Err(Reason::MalformedDataUri)); + continue; + }; + let media_type = uri.media_type(); + let sha256 = sha256_hex(&bytes); + let cached = signed_sources + .iter() + .find(|(sha, kind, _)| *sha == sha256 && *kind == media_type) + .map(|(_, _, result)| result.clone()); + let result = match cached { + Some(result) => result.map(|signed| (signed, Reason::CacheHit)), + None => { + let source = MediaSource { + page_url: &job.request.page_url, + reference: None, + origin_host: None, + mime: uri.declared_type(), + media_type: &media_type, + bytes: &bytes, + sha256: &sha256, + link_deliverable: false, + require_embedded_manifest: false, + }; + let result = self + .sign_media(services, &source, job.policy_digest, work, &mut calls) + .await + .and_then(|(signed, reason)| { + if media_type == SVG_MEDIA_TYPE { + embedded::verify_svg_carrier(&bytes, &signed.bytes)?; + } + Ok((signed, reason)) + }); + signed_sources.push(( + sha256.clone(), + media_type.clone(), + result + .as_ref() + .map(|(signed, _)| signed.clone()) + .map_err(|reason| *reason), + )); + result + } + }; + match result { + Ok((signed, reason)) => { + edits.push((found.start, found.end, uri.encode(&signed.bytes))); + outcomes.push(Ok(reason)); + } + Err(reason) => outcomes.push(Err(reason)), + } + } + let mut output = None; + if !edits.is_empty() { + output = embedded::splice(document, &edits, self.max_document_bytes); + if output.is_none() { + revoke(&mut outcomes); + } + } + if job.surface == Surface::Page { + let current = output.as_deref().unwrap_or(document); + let svgs = if output.is_some() { + embedded::scan_html(current, None).svgs + } else { + scan.svgs.clone() + }; + let mut svg_outcomes = Vec::with_capacity(svgs.len()); + let mut svg_edits = Vec::new(); + for (start, end) in svgs { + let Some(fragment) = current.get(start..end) else { + continue; + }; + match self + .sign_inline_svg(services, job, fragment, work, &mut calls) + .await + { + Ok((signed, reason)) => { + svg_edits.push((start, end, signed)); + svg_outcomes.push(Ok(reason)); + } + Err(reason) => svg_outcomes.push(Err(reason)), + } + } + if !svg_edits.is_empty() { + match embedded::splice(current, &svg_edits, self.max_document_bytes) { + Some(signed) => output = Some(signed), + None => revoke(&mut svg_outcomes), + } + } + outcomes.extend(svg_outcomes); + } + for outcome in &outcomes { + match outcome { + Ok(reason) => log_media(true, *reason), + Err(reason) => log_media(false, *reason), + } + } + output + } + + /// Sign one inline SVG as the standalone document + /// [`signable_svg`](svg_equivalence::signable_svg) proves equivalent to + /// the browser's tree: the original bytes where they already are one, + /// else the browser's tree serialized as XML. Those bytes are hashed, + /// sent, cached and compared with the answer, and the result is kept + /// only when the signer added its manifest and changed nothing else. + async fn sign_inline_svg( + &self, + services: &RuntimeServices, + job: &EmbeddedJob<'_>, + fragment: &str, + work: &mut Deadline<'_>, + calls: &mut usize, + ) -> Result<(String, Reason), Reason> { + let svg = svg_equivalence::signable_svg(fragment, self.max_media_bytes())?; + let bytes = svg.as_bytes(); + let sha256 = sha256_hex(bytes); + let source = MediaSource { + page_url: &job.request.page_url, + reference: None, + origin_host: None, + mime: SVG_MEDIA_TYPE, + media_type: SVG_MEDIA_TYPE, + bytes, + sha256: &sha256, + link_deliverable: false, + require_embedded_manifest: false, + }; + let (signed, reason) = self + .sign_media(services, &source, job.policy_digest, work, calls) + .await?; + embedded::verify_svg_carrier(bytes, &signed.bytes)?; + let signed = + String::from_utf8(signed.bytes.to_vec()).map_err(|_| Reason::InlineSvgAltered)?; + Ok((signed, reason)) + } + + /// The namespace core keeps this credential's signed representations + /// under: the module version, the policy digest the service answers now, + /// and the credential context hash. Never a secret. The policy is asked + /// for the claim's root, being the policy of the credential whatever the + /// asset, and asked live on every call, never from the article path's + /// cache, so a revoked credential or changed policy stops every kept copy + /// at once. + async fn delivery_namespace(&self, services: &RuntimeServices) -> Result { + let mut work = Deadline::new( + services.timer(), + Duration::from_millis(self.config.max_hold_ms).saturating_sub(SKIP_WRITE_RESERVE), + ); + work.start().await; + let claim_root = format!("https://{}/", self.config.domain); + let policy_digest = self + .request_policy(&claim_root, services, &mut work) + .await?; + Ok(self.kept_namespace(&policy_digest)) + } + + /// The namespace of signed representations made under `policy_digest` + /// with this credential, as [`Self::delivery_namespace`] answers it. + pub(super) fn kept_namespace(&self, policy_digest: &str) -> String { + format!("{NAMESPACE_VERSION}:{policy_digest}:{}", self.cache_context) + } + + /// Sign one binary representation core captured, for every response of + /// it: the reader-facing address must be on the claimed domain, the + /// bytes the ones core hashed. + async fn sign_delivery( + &self, + services: &RuntimeServices, + job: &MediaJob<'_>, + ) -> Result { + let reader_url = url::Url::parse(job.reader_url).map_err(|_| Reason::OutsideClaim)?; + let host = reader_url.host_str().ok_or(Reason::OutsideClaim)?; + if !matches!(reader_url.scheme(), "https" | "http") + || !self.claims_host(host) + || is_excluded_prefix(reader_url.path()) + { + return Err(Reason::OutsideClaim); + } + if job.source.len() > self.max_media_bytes() { + return Err(Reason::MediaTooLarge); + } + let sha256 = sha256_hex(job.source); + if sha256 != job.source_sha256 { + return Err(Reason::SourceDigestMismatch); + } + // The host core fetched from, which the endpoint checks the claim's + // managed origins and the reference against. + let origin_host = job + .source_origin_host + .filter(|host| valid_origin_host(host)) + .ok_or(Reason::InvalidOriginHost)?; + let media_type = canonical_media_type(job.content_type); + // No address serves the optimizer's answer, so it is bound by its + // own bytes and never by the address the optimizer read. + let reference = job.source_reference.filter(|reference| { + !job.optimizer_output && safe_source_reference(reference, &self.config.domain) + }); + let mut work = Deadline::new( + services.timer(), + Duration::from_millis(self.config.max_hold_ms).saturating_sub(SKIP_WRITE_RESERVE), + ); + work.start().await; + // Asked live, as for the namespace: a cached digest could outlive a + // revocation and reuse a signed result under it. + let policy_digest = self + .request_policy(job.reader_url, services, &mut work) + .await?; + let source = MediaSource { + page_url: job.reader_url, + reference, + origin_host: Some(origin_host), + mime: job.content_type, + media_type: &media_type, + bytes: job.source, + sha256: &sha256, + link_deliverable: true, + require_embedded_manifest: job.optimizer_output, + }; + // One representation, one call at most. + let mut calls = 0_usize; + let (signed, reason) = self + .sign_media(services, &source, &policy_digest, &mut work, &mut calls) + .await?; + if media_type == SVG_MEDIA_TYPE && signed.disposition == Disposition::Embedded { + embedded::verify_svg_carrier(job.source, &signed.bytes)?; + } + let mut headers: Vec = Vec::with_capacity(4); + if signed.disposition == Disposition::External + && let Some(link) = signed.link() + { + headers.push(( + header::LINK, + HeaderValue::from_str(&link).map_err(|_| Reason::InvalidSignedResponse)?, + )); + } + for (name, value) in [ + (INSTANCE_ID_HEADER, &signed.instance_id), + (DOCUMENT_ID_HEADER, &signed.document_id), + (VERIFICATION_URL_HEADER, &signed.verification_url), + ] { + headers.push(( + name, + HeaderValue::from_str(value).map_err(|_| Reason::InvalidSignedResponse)?, + )); + } + log_media(true, reason); + Ok(DeliveredMedia { + bytes: signed.bytes, + content_type: signed.content_type, + headers, + // The policy this representation was signed under, so core keeps + // it where only that policy finds it. + namespace: self.kept_namespace(&policy_digest), + }) + } +} + +/// Marks every signed outcome of a batch whose output would exceed the +/// document budget as unsigned: none of its edits is applied. +fn revoke(outcomes: &mut [Outcome]) { + for outcome in outcomes.iter_mut() { + if outcome.is_ok() { + *outcome = Err(Reason::OutputTooLarge); + } + } +} + +/// The version of the module's signed representations, part of every +/// namespace core keeps them under. +const NAMESPACE_VERSION: &str = concat!("media-v1:", env!("CARGO_PKG_VERSION")); + +/// The module's binary media signer, which core runs on publisher and asset +/// proxy responses of the media it captures. +pub(crate) struct EncypherMedia { + signer: Arc, +} + +impl EncypherMedia { + pub(crate) fn new(signer: Arc) -> Self { + Self { signer } + } +} + +impl std::fmt::Debug for EncypherMedia { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("EncypherMedia") + .finish_non_exhaustive() + } +} + +impl MediaSigner for EncypherMedia { + fn signer_id(&self) -> &'static str { + MODULE + } + + /// Every path but an API, feed, admin or well-known path: media is + /// recognized by the origin's media type, not by a suffix. + fn handles_path(&self, path: &str) -> bool { + !is_excluded_prefix(path) + } + + fn handles_media_type(&self, media_type: &str) -> bool { + media_type != MEDIA_SEGMENT_TYPE + && SIGNABLE_TYPE_PREFIXES + .iter() + .any(|prefix| media_type.starts_with(prefix) && media_type.len() > prefix.len()) + } + + fn max_source_bytes(&self) -> usize { + self.signer.max_media_bytes() + } + + /// Larger media, up to this, is signed through the prepared lane: hashed + /// where it is delivered and never uploaded. + fn max_large_source_bytes(&self) -> u64 { + self.signer.max_large_media_bytes() + } + + /// The one budget for a large source: core's whole-source hash and the + /// prepared lane together. + fn max_large_media_duration(&self) -> Duration { + self.signer.max_large_media_duration() + } + + fn sign_large<'a>( + &'a self, + services: &'a RuntimeServices, + job: &'a LargeMediaJob<'a>, + ) -> LargeMediaSigning<'a> { + Box::pin(async move { + // The asynchronous mode never signs in delivery; core asks + // `lookup_large` instead and never reaches this. + if self.signer.is_async() { + return Err(MediaSigningFailure { + reason: Reason::NotPrepared.code(), + }); + } + match self.signer.sign_large(services, job).await { + Ok(signed) => { + log_media(true, Reason::Signed); + Ok(signed) + } + Err(refusal) => { + // A source with its own embedded credentials is delivered + // as it is: its provenance is kept, it is not unsigned + // by failure and not newly signed. + log::info!( + "encypher: media outcome={} lane=large reason={}", + if refusal.retains_provenance() { + "retained" + } else { + "unsigned" + }, + refusal.code() + ); + Err(MediaSigningFailure { + reason: refusal.code(), + }) + } + } + }) + } + + fn namespace<'a>(&'a self, services: &'a RuntimeServices) -> MediaNamespacing<'a> { + Box::pin(async move { + self.signer + .delivery_namespace(services) + .await + .map_err(|reason| { + log_media(false, reason); + MediaSigningFailure { + reason: reason.code(), + } + }) + }) + } + + fn sign<'a>( + &'a self, + services: &'a RuntimeServices, + job: &'a MediaJob<'a>, + ) -> MediaSigning<'a> { + Box::pin(async move { + // The asynchronous mode looks up only what was published, and + // never falls back to signing in delivery. + let result = if self.signer.is_async() { + self.signer.published_delivery(services, job).await + } else { + self.signer.sign_delivery(services, job).await + }; + result.map_err(|reason| { + log_media(false, reason); + MediaSigningFailure { + reason: reason.code(), + } + }) + }) + } + + /// In the asynchronous mode, delivery only looks up published results. + fn lookup_only(&self) -> bool { + self.signer.is_async() + } + + fn lookup_large<'a>( + &'a self, + services: &'a RuntimeServices, + job: &'a LargeLookupJob<'a>, + ) -> LargeLookup<'a> { + Box::pin(async move { + self.signer + .published_large(services, job) + .await + .map_err(|reason| MediaSigningFailure { + reason: reason.code(), + }) + }) + } + + /// A request the module's preparer verified as a preparation snapshot. + fn observes_snapshot(&self, state: &IntegrationRequestState) -> bool { + self.signer.is_async() + && state + .get::(crate::INTEGRATION_ID) + .is_some() + } + + fn snapshot_headers( + &self, + state: &IntegrationRequestState, + facts: &SnapshotFacts, + ) -> Vec { + let (Some(preparation), Some(grant)) = ( + self.signer.preparation(), + state.get::(crate::INTEGRATION_ID), + ) else { + return Vec::new(); + }; + let origin = crate::preparation::edge::media_snapshot_origin(facts); + crate::preparation::edge::snapshot_facts(preparation, &grant, &origin) + .into_iter() + .collect() + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/provenance/encypher/src/processor/media/tests.rs b/crates/provenance/encypher/src/processor/media/tests.rs new file mode 100644 index 0000000000..91fa99c0aa --- /dev/null +++ b/crates/provenance/encypher/src/processor/media/tests.rs @@ -0,0 +1,1858 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use edgezero_core::key_value_store::KvPage; +use futures::executor::block_on; +use serde_json::json; +use sha2::{Digest as _, Sha256}; +use trusted_server_core::platform::test_support::{ + StoppedTimer, StubHttpClient, build_services_with_http_client, +}; +use trusted_server_core::platform::{KvError, PlatformHttpClient, PlatformKvStore}; + +use super::*; +use crate::EncypherConfig; + +const TOKEN: &str = "cwk_test_worker_token_value"; +const POLICY_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const PAGE_URL: &str = "https://publisher.example.com/news/story"; +const SIGN_URL: &str = "https://api.example.com/api/v1/public/cdn/media/sign"; +const SOURCE: &[u8] = b"\x89PNG\r\n\x1a\n-source-pixels"; +const SIGNED: &[u8] = b"\x89PNG\r\n\x1a\n-source-pixels-caBX-jumb-c2pa-manifest"; +const MANIFEST: &str = "QUJD"; + +/// A runtime KV store held in memory, recording deletes. +#[derive(Default)] +struct MemoryKv { + entries: Mutex>, + deletes: Mutex>, +} + +impl MemoryKv { + fn keys_with(&self, prefix: &str) -> Vec { + self.entries + .lock() + .expect("should lock entries") + .keys() + .filter(|key| key.starts_with(prefix)) + .cloned() + .collect() + } + + fn get(&self, key: &str) -> Option { + self.entries + .lock() + .expect("should lock entries") + .get(key) + .cloned() + } + + fn set(&self, key: &str, value: Bytes) { + self.entries + .lock() + .expect("should lock entries") + .insert(key.to_owned(), value); + } +} + +#[async_trait::async_trait(?Send)] +impl PlatformKvStore for MemoryKv { + async fn get_bytes(&self, key: &str) -> Result, KvError> { + Ok(self.get(key)) + } + + async fn put_bytes(&self, key: &str, value: Bytes) -> Result<(), KvError> { + self.set(key, value); + Ok(()) + } + + async fn put_bytes_with_ttl( + &self, + key: &str, + value: Bytes, + _ttl: Duration, + ) -> Result<(), KvError> { + self.set(key, value); + Ok(()) + } + + async fn delete(&self, key: &str) -> Result<(), KvError> { + self.entries + .lock() + .expect("should lock entries") + .remove(key); + self.deletes + .lock() + .expect("should lock deletes") + .push(key.to_owned()); + Ok(()) + } + + async fn list_keys_page( + &self, + _prefix: &str, + _cursor: Option<&str>, + _limit: usize, + ) -> Result { + Ok(KvPage::default()) + } +} + +fn signer() -> Arc { + let config: EncypherConfig = serde_json::from_value(json!({ + "api_base_url": "https://api.example.com", + "domain": "publisher.example.com", + "worker_token": TOKEN, + })) + .expect("should parse config"); + Arc::new(Signer::new(config, 5_000_000)) +} + +fn services(http: &Arc, kv: &Arc) -> RuntimeServices { + build_services_with_http_client(Arc::clone(http) as Arc) + .with_kv_store(Arc::clone(kv) as Arc) +} + +/// The signing headers of an embedded answer for `body`. +fn answer(body: &[u8], content_type: &str) -> Vec<(String, String)> { + [ + ("content-type", content_type.to_owned()), + ("x-encypher-policy-digest", POLICY_DIGEST.to_owned()), + ("x-encypher-signed-sha256", sha256_hex(body)), + ("x-encypher-manifest-instance-id", "xmp:iid:0001".to_owned()), + ("x-encypher-manifest-disposition", "embedded".to_owned()), + ("x-encypher-manifest-link-required", "false".to_owned()), + ( + "x-encypher-verification-url", + "https://verify.example.com/doc-1".to_owned(), + ), + ("x-encypher-document-id", "doc-1".to_owned()), + ] + .into_iter() + .map(|(name, value)| (name.to_owned(), value)) + .collect() +} + +/// The signing headers of a remote-manifest answer that needs a `Link`. +fn external_answer(body: &[u8]) -> Vec<(String, String)> { + let mut headers: Vec<(String, String)> = answer(body, "image/png") + .into_iter() + .filter(|(name, _)| { + name != "x-encypher-manifest-disposition" && name != "x-encypher-manifest-link-required" + }) + .collect(); + for (name, value) in [ + ("x-encypher-manifest-disposition", "external"), + ("x-encypher-manifest-link-required", "true"), + ( + "x-encypher-manifest-url", + "https://api.example.com/manifests/doc-1", + ), + ("x-encypher-manifest-sha256", POLICY_DIGEST), + ] { + headers.push((name.to_owned(), value.to_owned())); + } + headers +} + +fn png_source(sha256: &str) -> MediaSource<'_> { + MediaSource { + page_url: PAGE_URL, + reference: None, + origin_host: None, + mime: "image/png", + media_type: "image/png", + bytes: SOURCE, + sha256, + link_deliverable: false, + require_embedded_manifest: false, + } +} + +fn sign( + signer: &Signer, + services: &RuntimeServices, + source: &MediaSource<'_>, +) -> Result<(SignedMedia, Reason), Reason> { + block_on(async { + let mut work = Deadline::Unbounded; + let mut calls = 0; + signer + .sign_media(services, source, POLICY_DIGEST, &mut work, &mut calls) + .await + }) +} + +#[test] +fn a_document_obeys_its_configured_call_limit_for_distinct_assets() { + for limit in [3, 12] { + let http = Arc::new(StubHttpClient::new()); + let images: Vec> = (0..=limit) + .map(|index| { + let mut bytes = SOURCE.to_vec(); + bytes.push(u8::try_from(index).expect("should fit")); + bytes + }) + .collect(); + for image in images.iter().take(limit) { + let mut signed = image.clone(); + signed.extend_from_slice(b"-c2pa"); + let headers = answer(&signed, "image/png"); + http.push_response_with_headers(200, signed, headers); + } + let kv = Arc::new(MemoryKv::default()); + let document: String = images + .iter() + .map(|image| { + format!( + "", + STANDARD.encode(image) + ) + }) + .collect(); + let mut config = signer().config.clone(); + config.max_media_sign_calls_per_document = limit; + let signer = Signer::new(config, 5_000_000); + let services = services(&http, &kv); + let request = page_request(); + let scan = embedded::scan_html(&document, None); + let output = block_on(async { + let mut work = Deadline::Unbounded; + signer + .sign_embedded( + &services, + &embedded_job(&request, Surface::Page), + &document, + &scan, + &mut work, + ) + .await + }) + .expect("should sign the items within the configured limit"); + + assert_eq!( + http.recorded_request_methods().len(), + limit, + "should stop calling at the configured bound" + ); + for image in images.iter().take(limit) { + let mut signed = image.clone(); + signed.extend_from_slice(b"-c2pa"); + assert!( + output.contains(&STANDARD.encode(&signed)), + "should deliver every signed distinct asset within the limit" + ); + } + let last = images.last().expect("should have a last image"); + assert!( + output.contains(&STANDARD.encode(last)), + "should leave the deferred item as written" + ); + assert_eq!( + kv.keys_with(MEDIA_SKIP_KEY_PREFIX).len(), + 0, + "a deferred item should not be negatively cached" + ); + } +} + +fn header<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> { + headers + .iter() + .find(|(candidate, _)| candidate.eq_ignore_ascii_case(name)) + .map(|(_, value)| value.as_str()) +} + +fn contains(haystack: &[u8], needle: &[u8]) -> bool { + haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +#[test] +fn signs_exact_bytes_once_then_serves_the_verified_cached_answer() { + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let signer = signer(); + let sha = sha256_hex(SOURCE); + + let (first, reason) = sign(&signer, &services, &png_source(&sha)).expect("should sign"); + + assert_eq!(reason, Reason::Signed); + assert_eq!(first.bytes.as_ref(), SIGNED, "should keep the signed bytes"); + assert_eq!(http.recorded_request_methods(), ["POST"]); + assert_eq!(http.recorded_request_uris(), [SIGN_URL]); + let headers = &http.recorded_request_headers()[0]; + let key = header(headers, "idempotency-key").expect("should send a replay key"); + assert!( + key.len() == 64 && key.bytes().all(|byte| byte.is_ascii_hexdigit()), + "should derive a hex replay key: {key}" + ); + assert!( + header(headers, "x-encypher-worker-token").is_some(), + "should authenticate with the worker token" + ); + assert!( + header(headers, "content-type") + .is_some_and(|value| value.starts_with("multipart/form-data; boundary=")), + "should send multipart" + ); + let body = &http.recorded_request_bodies()[0]; + assert!(contains(body, SOURCE), "should send the exact source bytes"); + assert!( + contains(body, b"\"reference\":null"), + "should send no reference for media without a safe address" + ); + assert!( + contains(body, b"\"origin_host\":null"), + "should send no origin host for media fetched from no host" + ); + assert!( + contains( + body, + format!("\"expected_policy_digest\":\"{POLICY_DIGEST}\"").as_bytes() + ), + "should send the held policy digest" + ); + + let (second, reason) = sign(&signer, &services, &png_source(&sha)).expect("should sign"); + assert_eq!(reason, Reason::CacheHit); + assert_eq!( + second.bytes.as_ref(), + SIGNED, + "should replay identical bytes" + ); + assert_eq!( + http.recorded_request_methods().len(), + 1, + "should make no second call" + ); +} + +#[test] +fn a_stale_policy_evicts_the_digest_and_caches_a_skip() { + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers( + 409, + serde_json::to_vec(&json!({ + "success": false, + "error": {"code": "TRUSTED_SERVER_POLICY_CHANGED", "message": "stale"}, + })) + .expect("should encode"), + vec![("content-type", "application/json")], + ); + let kv = Arc::new(MemoryKv::default()); + let signer = signer(); + kv.set(&signer.policy_key, Bytes::from_static(b"{}")); + let services = services(&http, &kv); + let sha = sha256_hex(SOURCE); + + assert_eq!( + sign(&signer, &services, &png_source(&sha)).expect_err("should not sign"), + Reason::PolicyChanged + ); + assert!( + kv.get(&signer.policy_key).is_none(), + "should evict the policy" + ); + assert_eq!( + kv.keys_with(MEDIA_SKIP_KEY_PREFIX).len(), + 1, + "should cache a skip" + ); + assert_eq!( + sign(&signer, &services, &png_source(&sha)).expect_err("should not sign"), + Reason::CachedSkip + ); + assert_eq!( + http.recorded_request_methods().len(), + 1, + "should not call again" + ); +} + +#[test] +fn a_purged_result_is_asked_for_again_under_a_new_key_on_the_next_view() { + for (status, code) in [ + (410, "SIGNING_RESULT_PURGED"), + (409, "SIGNING_IDEMPOTENCY_CONFLICT"), + ] { + let http = Arc::new(StubHttpClient::new()); + http.push_response( + status, + serde_json::to_vec(&json!({"success": false, "error": {"code": code}})) + .expect("should encode"), + ); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let signer = signer(); + let sha = sha256_hex(SOURCE); + + let first = sign(&signer, &services, &png_source(&sha)).expect_err("should not sign"); + assert!( + matches!(first, Reason::ResultPurged | Reason::IdempotencyConflict), + "{code}: should report {first:?}" + ); + assert_eq!( + http.recorded_request_methods().len(), + 1, + "{code}: should not retry" + ); + assert!( + kv.keys_with(MEDIA_SKIP_KEY_PREFIX).is_empty(), + "{code}: no skip" + ); + + let (_, reason) = sign(&signer, &services, &png_source(&sha)).expect("should sign"); + assert_eq!(reason, Reason::Signed); + let headers = http.recorded_request_headers(); + assert_ne!( + header(&headers[0], "idempotency-key"), + header(&headers[1], "idempotency-key"), + "{code}: should mint a new replay key" + ); + } +} + +#[test] +fn a_tampered_cached_answer_is_refused_and_evicted() { + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let signer = signer(); + let sha = sha256_hex(SOURCE); + sign(&signer, &services, &png_source(&sha)).expect("should sign"); + let key = kv.keys_with(MEDIA_KEY_PREFIX)[0].clone(); + let mut entry: serde_json::Value = + serde_json::from_slice(&kv.get(&key).expect("should hold the answer")).expect("JSON"); + entry["bytes"] = json!(STANDARD.encode(b"\x89PNG\r\n\x1a\nother-c2pa")); + kv.set(&key, Bytes::from(serde_json::to_vec(&entry).expect("JSON"))); + + assert_eq!( + sign(&signer, &services, &png_source(&sha)).expect_err("should refuse"), + Reason::CachedEntryInvalid + ); + assert!(kv.get(&key).is_none(), "should evict the tampered entry"); +} + +#[test] +fn unsigned_or_mismatched_answers_are_never_used_and_are_negatively_cached() { + let other_policy = "b".repeat(64); + let cases = [ + ( + SOURCE.to_vec(), + answer(SOURCE, "image/png"), + Reason::UnsignedResponse, + ), + ( + b"\x89PNG\r\n\x1a\nswapped-c2pa".to_vec(), + answer(SIGNED, "image/png"), + Reason::SignedDigestMismatch, + ), + ( + SIGNED.to_vec(), + answer(SIGNED, "image/jpeg"), + Reason::SignedTypeMismatch, + ), + ( + SIGNED.to_vec(), + answer(SIGNED, "image/png") + .into_iter() + .map(|(name, value)| { + if name == "x-encypher-policy-digest" { + (name, other_policy.clone()) + } else { + (name, value) + } + }) + .collect(), + Reason::PolicyDigestMismatch, + ), + ( + SIGNED.to_vec(), + external_answer(SIGNED), + Reason::ManifestUndeliverable, + ), + ( + SIGNED.to_vec(), + answer(SIGNED, "image/png") + .into_iter() + .map(|(name, value)| { + if name == "x-encypher-verification-url" { + ( + name, + "http://localhost:9000/api/v1/verify/cdnmedia_dev".to_owned(), + ) + } else { + (name, value) + } + }) + .collect(), + Reason::InvalidSignedResponse, + ), + ]; + for (body, headers, expected) in cases { + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers(200, body, headers); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let sha = sha256_hex(SOURCE); + + assert_eq!( + sign(&signer(), &services, &png_source(&sha)).expect_err("should not sign"), + expected + ); + assert!( + kv.keys_with(MEDIA_KEY_PREFIX).is_empty(), + "{expected:?}: should cache no answer" + ); + assert_eq!( + kv.keys_with(MEDIA_SKIP_KEY_PREFIX).len(), + 1, + "{expected:?}: should cache the failure" + ); + } +} + +fn embedded_job<'a>(request: &'a PageRequest, surface: Surface) -> EmbeddedJob<'a> { + EmbeddedJob { + request, + surface, + policy_digest: POLICY_DIGEST, + } +} + +fn page_request() -> PageRequest { + PageRequest { + method: Method::GET, + page_url: PAGE_URL.to_owned(), + query: String::new(), + removed_prefix: String::new(), + response_status: 200, + origin_response_is_private: false, + } +} + +fn sign_document(services: &RuntimeServices, document: &str, surface: Surface) -> Option { + sign_document_with_policies(services, document, surface, None) +} + +fn sign_document_with_policies( + services: &RuntimeServices, + document: &str, + surface: Surface, + policies: Option<&[String]>, +) -> Option { + let request = page_request(); + let scan = match surface { + Surface::Page => embedded::scan_html(document, policies), + Surface::Stylesheet => embedded::scan_stylesheet(document), + }; + block_on(async { + let mut work = Deadline::Unbounded; + signer() + .sign_embedded( + services, + &embedded_job(&request, surface), + document, + &scan, + &mut work, + ) + .await + }) +} + +#[test] +fn embedded_data_uris_are_replaced_by_their_signed_bytes_once_per_source() { + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + let kv = Arc::new(MemoryKv::default()); + let source = format!("data:image/png;base64,{}", STANDARD.encode(SOURCE)); + let signed = format!("data:image/png;base64,{}", STANDARD.encode(SIGNED)); + let document = format!( + "

x

a" + ); + + let output = sign_document(&services(&http, &kv), &document, Surface::Page) + .expect("should sign the page's media"); + + assert_eq!( + output, + format!( + "

x

a" + ), + "should replace each URI and change nothing else" + ); + assert_eq!( + http.recorded_request_methods(), + ["POST"], + "should sign one source once" + ); +} + +#[test] +fn a_failed_embedded_signature_leaves_the_uri_as_written() { + let http = Arc::new(StubHttpClient::new()); + http.push_response( + 422, + b"{\"error\":{\"code\":\"UNSUPPORTED_MEDIA_TYPE\"}}".to_vec(), + ); + let kv = Arc::new(MemoryKv::default()); + let document = format!( + "", + STANDARD.encode(SOURCE) + ); + + assert_eq!( + sign_document(&services(&http, &kv), &document, Surface::Page), + None, + "should leave the page as written" + ); +} + +#[test] +fn stylesheet_media_is_signed_in_place() { + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + let kv = Arc::new(MemoryKv::default()); + let document = format!( + ".hero{{background:url('data:image/png;base64,{}') no-repeat}}", + STANDARD.encode(SOURCE) + ); + + let output = sign_document(&services(&http, &kv), &document, Surface::Stylesheet) + .expect("should sign the stylesheet's media"); + + assert_eq!( + output, + format!( + ".hero{{background:url('data:image/png;base64,{}') no-repeat}}", + STANDARD.encode(SIGNED) + ) + ); +} + +#[test] +fn inline_svg_is_signed_as_its_exact_bytes_and_kept_only_with_the_writer_s_carrier_alone() { + let root = ""; + let body = ""; + let svg = format!("{root}{body}"); + let document = format!("

x

{svg}

y

"); + let signed_svg = format!("{root}{MANIFEST}{body}"); + let restyled = signed_svg.replace("currentColor", "#000"); + for (answer_svg, expected) in [ + ( + signed_svg.clone(), + Some(format!("

x

{signed_svg}

y

")), + ), + (restyled, None), + ] { + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers( + 200, + answer_svg.clone().into_bytes(), + answer(answer_svg.as_bytes(), "image/svg+xml"), + ); + let kv = Arc::new(MemoryKv::default()); + + let output = sign_document(&services(&http, &kv), &document, Surface::Page); + + assert_eq!( + output, expected, + "should keep only a pure manifest insertion" + ); + assert!( + contains(&http.recorded_request_bodies()[0], svg.as_bytes()), + "should send the element's exact bytes" + ); + } +} + +#[test] +fn inline_svg_the_browser_reads_differently_is_signed_as_its_parsed_tree() { + // The browser adds no namespace and keeps the first `viewBox`; what is + // hashed, sent, compared and placed is that tree as standalone XML. + let original = ""; + let serialized = crate::svg_equivalence::signable_svg(original, 1024 * 1024) + .expect("should have an equivalent standalone document") + .into_owned(); + assert_ne!(serialized, original, "should not sign the bytes as written"); + let document = roxmltree::Document::parse(&serialized).expect("should be standalone XML"); + assert_eq!( + document.root_element().attribute("viewBox"), + Some("0 0 2 2"), + "should keep the browser's first viewBox" + ); + // The writer's carrier goes right after the root start tag, whose + // attribute values escape any `>`. + let (root, body) = + serialized.split_at(serialized.find('>').expect("should close the root") + 1); + let signed_svg = format!("{root}{MANIFEST}{body}"); + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers( + 200, + signed_svg.clone().into_bytes(), + answer(signed_svg.as_bytes(), "image/svg+xml"), + ); + let kv = Arc::new(MemoryKv::default()); + + let output = sign_document( + &services(&http, &kv), + &format!("

x

{original}

y

"), + Surface::Page, + ); + + assert_eq!( + output, + Some(format!("

x

{signed_svg}

y

")), + "should place the signed serialization where the element was" + ); + let sent = &http.recorded_request_bodies()[0]; + assert!( + contains(sent, serialized.as_bytes()), + "should send the serialized tree" + ); + assert!( + !contains(sent, original.as_bytes()), + "should not send bytes the browser reads differently" + ); +} + +#[test] +fn unsigned_self_closing_svgs_never_add_whole_asset_signing_calls() { + let http = Arc::new(StubHttpClient::new()); + let kv = Arc::new(MemoryKv::default()); + assert_eq!( + sign_document(&services(&http, &kv), "", Surface::Page), + None + ); + assert!(http.recorded_request_methods().is_empty()); + + let source = format!("data:image/png;base64,{}", STANDARD.encode(SOURCE)); + let signed = format!("data:image/png;base64,{}", STANDARD.encode(SIGNED)); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + assert_eq!( + sign_document( + &services(&http, &kv), + &format!(""), + Surface::Page, + ), + Some(format!("")) + ); + assert_eq!(http.recorded_request_methods(), ["POST"]); +} + +#[test] +fn original_svg_provenance_makes_no_child_or_whole_asset_signing_calls() { + let source = format!("data:image/png;base64,{}", STANDARD.encode(SOURCE)); + for provenance in [ + MANIFEST, + "", + "https://example.com/m", + "", + "", + ] { + let http = Arc::new(StubHttpClient::new()); + let kv = Arc::new(MemoryKv::default()); + let document = format!( + "\ + {provenance}" + ); + assert_eq!( + sign_document_with_policies(&services(&http, &kv), &document, Surface::Page, Some(&[]),), + None, + "must preserve the entire original range for {provenance}" + ); + assert!(http.recorded_request_methods().is_empty(), "{provenance}"); + } +} + +#[test] +fn signing_an_outside_image_cannot_change_original_svg_provenance_ranges() { + let source = format!("data:image/png;base64,{}", STANDARD.encode(SOURCE)); + let signed = format!("data:image/png;base64,{}", STANDARD.encode(SIGNED)); + let svg = format!( + "{MANIFEST}\ + \ + " + ); + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + let kv = Arc::new(MemoryKv::default()); + let document = format!("{svg}"); + assert_eq!( + sign_document_with_policies(&services(&http, &kv), &document, Surface::Page, Some(&[]),), + Some(format!("{svg}")), + "the rescan after outside edits must still preserve every SVG byte" + ); + assert_eq!(http.recorded_request_methods(), ["POST"]); +} + +#[test] +fn an_unsigned_svg_still_signs_its_attribute_css_and_then_the_whole_asset() { + let source = format!("data:image/png;base64,{}", STANDARD.encode(SOURCE)); + let signed = format!("data:image/png;base64,{}", STANDARD.encode(SIGNED)); + let root = ""; + let body = format!( + "Signed with C2PA\ + " + ); + let source_svg = format!("{root}{body}"); + let child_signed_svg = source_svg.replace(&source, &signed); + let signed_svg = format!("{root}{MANIFEST}{}", body.replace(&source, &signed)); + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + http.push_response_with_headers( + 200, + signed_svg.as_bytes().to_vec(), + answer(signed_svg.as_bytes(), "image/svg+xml"), + ); + let kv = Arc::new(MemoryKv::default()); + assert_eq!( + sign_document_with_policies(&services(&http, &kv), &source_svg, Surface::Page, Some(&[]),), + Some(signed_svg) + ); + assert_eq!(http.recorded_request_methods(), ["POST", "POST"]); + let requests = http.recorded_request_bodies(); + assert!(contains(&requests[0], SOURCE)); + assert!(contains(&requests[1], child_signed_svg.as_bytes())); + assert!(!contains(&requests[1], source_svg.as_bytes())); +} + +#[test] +fn inline_svg_that_breaks_out_is_reported_without_a_call() { + let http = Arc::new(StubHttpClient::new()); + let kv = Arc::new(MemoryKv::default()); + + let output = sign_document( + &services(&http, &kv), + "

x

", + Surface::Page, + ); + + assert_eq!(output, None); + assert!( + http.recorded_request_methods().is_empty(), + "should not call" + ); +} + +fn job<'a>( + reader_url: &'a str, + source: &'a Bytes, + sha256: &'a str, + reference: Option<&'a str>, + origin_host: Option<&'a str>, +) -> MediaJob<'a> { + MediaJob { + reader_url, + reader_query: "", + content_type: "image/png", + source, + source_sha256: sha256, + source_reference: reference, + source_origin_host: origin_host, + optimizer_output: false, + } +} + +fn deliver( + services: &RuntimeServices, + job: &MediaJob<'_>, +) -> Result { + let media = EncypherMedia::new(signer()); + block_on(media.sign(services, job)) +} + +const ORIGIN: Option<&str> = Some("origin.example.com"); + +#[test] +fn delivered_media_outside_the_claim_with_other_bytes_or_no_origin_host_is_refused() { + let http = Arc::new(StubHttpClient::new()); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let source = Bytes::from_static(SOURCE); + let sha = sha256_hex(SOURCE); + let reader = "https://publisher.example.com/a.png"; + + let outside = deliver( + &services, + &job("https://cdn.example.org/a.png", &source, &sha, None, ORIGIN), + ) + .expect_err("should refuse another host"); + assert_eq!(outside.reason, "outside_claim"); + let other = "0".repeat(64); + let mismatch = deliver(&services, &job(reader, &source, &other, None, ORIGIN)) + .expect_err("should refuse other bytes"); + assert_eq!(mismatch.reason, "source_digest_mismatch"); + for host in [ + None, + Some(""), + Some("origin.example.com:8443"), + Some("user@origin.example.com"), + Some("origin.example.com/images"), + Some("Origin.example.com"), + ] { + let refused = deliver(&services, &job(reader, &source, &sha, None, host)) + .expect_err("should refuse a missing or malformed origin host"); + assert_eq!(refused.reason, "invalid_origin_host", "{host:?}"); + } + assert!( + http.recorded_request_methods().is_empty(), + "should not call" + ); +} + +#[test] +fn delivered_media_carries_a_link_for_a_remote_manifest_and_only_a_safe_reference() { + for (reference, origin_host, expected_reference, expected_origin) in [ + ( + Some("https://publisher.example.com/images/a.png"), + "publisher.example.com", + "\"reference\":\"https://publisher.example.com/images/a.png\"", + "\"origin_host\":\"publisher.example.com\"", + ), + ( + Some("https://origin.example.com/images/a.png"), + "origin.example.com", + "\"reference\":null", + "\"origin_host\":\"origin.example.com\"", + ), + // A fetched address with a query is no reference, but its host is + // still the origin's. + ( + None, + "origin.example.com", + "\"reference\":null", + "\"origin_host\":\"origin.example.com\"", + ), + ] { + let http = Arc::new(StubHttpClient::new()); + http.push_response( + 200, + serde_json::to_vec(&json!({"policy_digest": POLICY_DIGEST})).expect("JSON"), + ); + http.push_response_with_headers(200, SOURCE.to_vec(), external_answer(SOURCE)); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let source = Bytes::from_static(SOURCE); + let sha = sha256_hex(SOURCE); + + let delivered = deliver( + &services, + &job( + "https://publisher.example.com/images/a.png", + &source, + &sha, + reference, + Some(origin_host), + ), + ) + .expect("should sign"); + + assert_eq!( + delivered.bytes.as_ref(), + SOURCE, + "should deliver the answer's bytes" + ); + assert_eq!(delivered.content_type, "image/png"); + assert_eq!( + delivered.namespace, + format!( + "{NAMESPACE_VERSION}:{POLICY_DIGEST}:{}", + signer().cache_context + ), + "should name the policy it was signed under for core's kept copy" + ); + let named = |name: &str| { + delivered + .headers + .iter() + .find(|(header, _)| header.as_str() == name) + .map(|(_, value)| value.to_str().expect("text").to_owned()) + }; + assert_eq!( + named("link").as_deref(), + Some("; rel=\"c2pa-manifest\""), + "should name the remote manifest on every response" + ); + assert_eq!(named("x-encypher-document-id").as_deref(), Some("doc-1")); + let body = &http.recorded_request_bodies()[1]; + assert!( + contains(body, expected_reference.as_bytes()), + "should send {expected_reference}" + ); + assert!( + contains(body, expected_origin.as_bytes()), + "should send {expected_origin}" + ); + assert!( + contains(body, b"\"http_link_header\":true"), + "should say a Link can be delivered" + ); + } +} + +#[test] +fn an_optimizer_answer_is_sent_requiring_its_manifest_and_bound_by_its_bytes() { + let http = Arc::new(StubHttpClient::new()); + http.push_response( + 200, + serde_json::to_vec(&json!({"policy_digest": POLICY_DIGEST})).expect("JSON"), + ); + http.push_response_with_headers(200, SOURCE.to_vec(), external_answer(SOURCE)); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let source = Bytes::from_static(SOURCE); + let sha = sha256_hex(SOURCE); + let optimized = MediaJob { + optimizer_output: true, + ..job( + "https://publisher.example.com/images/a.png", + &source, + &sha, + Some("https://publisher.example.com/images/a.png"), + Some("publisher.example.com"), + ) + }; + + deliver(&services, &optimized).expect("should sign"); + + let body = &http.recorded_request_bodies()[1]; + assert!( + contains(body, b"\"require_embedded_manifest\":true"), + "should require the optimizer's embedded manifest" + ); + assert!( + contains(body, b"\"reference\":null"), + "should not name the address the optimizer read as the source" + ); + assert!( + contains(body, b"\"operations\":[]"), + "should report no operation of the edge's own" + ); +} + +#[test] +fn a_result_is_never_reused_for_the_same_bytes_from_another_origin() { + let http = Arc::new(StubHttpClient::new()); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + let kv = Arc::new(MemoryKv::default()); + let services = services(&http, &kv); + let signer = signer(); + let sha = sha256_hex(SOURCE); + + for host in ["origin-a.example.com", "origin-b.example.com"] { + let source = MediaSource { + origin_host: Some(host), + ..png_source(&sha) + }; + let (_, reason) = sign(&signer, &services, &source).expect("should sign"); + assert_eq!(reason, Reason::Signed, "{host}: should ask the endpoint"); + } + assert_eq!( + http.recorded_request_methods().len(), + 2, + "should bind each result to its origin" + ); + let headers = http.recorded_request_headers(); + assert_ne!( + header(&headers[0], "idempotency-key"), + header(&headers[1], "idempotency-key"), + "should replay neither under the other's key" + ); +} + +#[test] +fn media_is_recognized_by_its_type_on_every_path_but_excluded_prefixes() { + let media = EncypherMedia::new(signer()); + for path in [ + "/images/a.png", + "/video/clip.MOV", + "/audio/a.mp3", + "/img/123", + "/media", + ] { + assert!(media.handles_path(path), "should consider {path}"); + } + for path in [ + "/api/v1/a.png", + "/wp-admin/x.jpg", + "/.well-known/a.png", + "/feed/", + ] { + assert!(!media.handles_path(path), "should exclude {path}"); + } + for media_type in [ + "image/webp", + "image/svg+xml", + "video/mp4", + "video/webm", + "audio/mpeg", + "font/woff2", + ] { + assert!( + media.handles_media_type(media_type), + "should sign {media_type}" + ); + } + for media_type in [ + "image/", + "text/css", + "text/html", + "application/pdf", + "video/iso.segment", + ] { + assert!( + !media.handles_media_type(media_type), + "should not sign {media_type}" + ); + } + assert_eq!(media.max_source_bytes(), 5_000_000, "should bound captures"); +} + +#[test] +fn the_kept_copy_namespace_fails_closed_on_revocation_however_warm_the_caches() { + let http = Arc::new(StubHttpClient::new()); + http.push_response( + 200, + serde_json::to_vec(&json!({"policy_digest": POLICY_DIGEST})).expect("JSON"), + ); + http.push_response(403, b"{}".to_vec()); + let kv = Arc::new(MemoryKv::default()); + let runtime = services(&http, &kv); + let media = EncypherMedia::new(signer()); + // The article path's policy cache holds a fresh digest throughout. + let signer = signer(); + kv.set( + &signer.policy_key, + Bytes::from( + serde_json::to_vec(&json!({ + "kind": "digest", + "policy_digest": POLICY_DIGEST, + "expires_at": unix_now() + 300, + })) + .expect("JSON"), + ), + ); + + let namespace = block_on(media.namespace(&runtime)).expect("should answer a namespace"); + assert!( + namespace.contains(POLICY_DIGEST), + "should bind the policy digest: {namespace}" + ); + assert!( + !namespace.contains(TOKEN), + "should never carry the credential" + ); + let failure = block_on(media.namespace(&runtime)) + .expect_err("should refuse a revoked credential with the cached digest still fresh"); + assert_eq!(failure.reason, "forbidden"); +} + +#[test] +fn a_warm_signed_result_is_not_delivered_under_a_revoked_credential() { + let http = Arc::new(StubHttpClient::new()); + http.push_response( + 200, + serde_json::to_vec(&json!({"policy_digest": POLICY_DIGEST})).expect("JSON"), + ); + http.push_response_with_headers(200, SIGNED.to_vec(), answer(SIGNED, "image/png")); + http.push_response(403, b"{}".to_vec()); + let kv = Arc::new(MemoryKv::default()); + let runtime = services(&http, &kv); + let source = Bytes::from_static(SOURCE); + let sha = sha256_hex(SOURCE); + let job = job( + "https://publisher.example.com/images/a.png", + &source, + &sha, + None, + ORIGIN, + ); + deliver(&runtime, &job).expect("should sign and cache the result"); + assert!( + !kv.keys_with(MEDIA_KEY_PREFIX).is_empty(), + "should hold the signed result" + ); + + let refused = deliver(&runtime, &job).expect_err("should refuse the revoked credential"); + assert_eq!(refused.reason, "forbidden"); + assert_eq!( + http.recorded_request_methods(), + ["GET", "POST", "GET"], + "should neither sign again nor deliver the cached result" + ); +} + +// --- asynchronous prepared mode: lookups only ------------------------------ + +const READER: &str = "https://publisher.example.com/media/a.png"; +const LINK: &str = "; rel=\"c2pa-manifest\""; + +fn async_signer() -> Arc { + let config: EncypherConfig = serde_json::from_value(json!({ + "api_base_url": "https://api.example.com", + "domain": "publisher.example.com", + "worker_token": TOKEN, + "execution": "async_prepared", + "preparation": { + "worker_url": "https://prep.publisher.example.com", + "loopback_url": "https://publisher.example.com", + "deployment_id": "test", + "public_host": "publisher.example.com", + "signal_secret": "signal-secret-for-tests", + "publication_secret": "publication-secret-for-tests", + "routes": ["/media/"], + }, + })) + .expect("should parse config"); + Arc::new(Signer::new(config, 5_000_000)) +} + +fn async_variant_signer(reader_trigger: bool) -> Arc { + let mut config = async_signer().config().clone(); + let preparation = config + .preparation + .as_mut() + .expect("should configure preparation"); + preparation.reader_trigger = reader_trigger; + preparation.variants = serde_json::from_value(json!([ + {"name": "asset", "values": ["b"]}, + {"name": "edition", "values": ["uk", "us"]}, + ])) + .expect("should parse variant schema"); + Arc::new(Signer::new(config, 5_000_000)) +} + +fn policy_http() -> Arc { + let http = Arc::new(StubHttpClient::new()); + http.push_response( + 200, + serde_json::to_vec(&json!({"policy_digest": POLICY_DIGEST})).expect("should encode"), + ); + http +} + +fn standalone_identity(signer: &Signer, source: &[u8]) -> String { + crate::preparation::protocol::MediaIdentity { + credential_context: signer.credential_context(), + policy_digest: POLICY_DIGEST, + page_url: READER, + variant: "", + carrier: crate::preparation::protocol::Carrier::External, + mime: "image/png", + source_sha256: &sha256_hex(source), + } + .digest() +} + +fn publish_record(kv: &MemoryKv, identity: &str, result: &serde_json::Value) { + kv.set( + &crate::preparation::protocol::result_key(identity), + Bytes::from( + serde_json::to_vec(&json!({ + "protocol": crate::preparation::protocol::PREPARATION_PROTOCOL, + "identity": identity, + "retirement_epoch": 0, + "result": result, + })) + .expect("should encode"), + ), + ); +} + +fn assert_lookup_only(http: &StubHttpClient) { + let uris = http.recorded_request_uris(); + assert!( + uris.iter() + .all(|uri| uri.contains("/trusted-server/policy")), + "only the policy authority lookup should leave the instance: {uris:?}" + ); +} + +#[test] +fn async_standalone_media_serves_the_origin_bytes_with_the_published_link_or_goes_unsigned() { + let signer = async_signer(); + let media = EncypherMedia::new(Arc::clone(&signer)); + assert!( + media.lookup_only(), + "core should keep and load nothing for it" + ); + let source = Bytes::from_static(SOURCE); + let sha = sha256_hex(SOURCE); + let kv = Arc::new(MemoryKv::default()); + publish_record( + &kv, + &standalone_identity(&signer, SOURCE), + &json!({ + "kind": "external", + "source_sha256": sha, + "headers": [["link", LINK], ["x-encypher-manifest-instance-id", "xmp:iid:0001"]], + }), + ); + + let http = policy_http(); + let delivered = block_on(media.sign( + &services(&http, &kv), + &job(READER, &source, &sha, Some(READER), ORIGIN), + )) + .expect("should find the published result"); + assert_eq!( + delivered.bytes, source, + "should deliver the origin's own bytes" + ); + assert!( + delivered + .headers + .iter() + .any(|(name, value)| name == "link" && value.to_str().ok() == Some(LINK)), + "should carry the published manifest link" + ); + assert_lookup_only(&http); + + // A changed image has its own identity, which nothing was published for. + let changed = Bytes::from_static(SIGNED); + let changed_sha = sha256_hex(SIGNED); + let http = policy_http(); + let missed = block_on(media.sign( + &services(&http, &kv), + &job(READER, &changed, &changed_sha, Some(READER), ORIGIN), + )) + .expect_err("should find nothing for changed bytes"); + assert_eq!(missed.reason, "not_prepared"); + assert_lookup_only(&http); +} + +#[test] +fn async_standalone_media_selects_its_admitted_variant_not_the_base_publication() { + let signer = async_variant_signer(false); + let media = EncypherMedia::new(Arc::clone(&signer)); + let source = Bytes::from_static(SOURCE); + let sha = sha256_hex(SOURCE); + let kv = Arc::new(MemoryKv::default()); + for (variant, instance) in [("", "base"), ("asset=b&edition=uk", "variant")] { + let identity = crate::preparation::protocol::MediaIdentity { + credential_context: signer.credential_context(), + policy_digest: POLICY_DIGEST, + page_url: READER, + variant, + carrier: crate::preparation::protocol::Carrier::External, + mime: "image/png", + source_sha256: &sha, + } + .digest(); + publish_record( + &kv, + &identity, + &json!({ + "kind": "external", + "source_sha256": sha, + "headers": [["link", LINK], ["x-encypher-manifest-instance-id", instance]], + }), + ); + } + // Bounded delivery hashes the actual body, so query ordering aliases + // can use the canonical identity without trusting an origin validator. + for reader_query in ["asset=b&edition=uk", "edition=uk&asset=b"] { + let http = policy_http(); + let delivered = block_on(media.sign( + &services(&http, &kv), + &MediaJob { + reader_query, + ..job(READER, &source, &sha, None, ORIGIN) + }, + )) + .expect("should find the prepared variant"); + assert!( + delivered.headers.iter().any(|(name, value)| { + name == "x-encypher-manifest-instance-id" && value == "variant" + }), + "must select the variant's publication, not the identical-byte base record" + ); + assert_eq!(delivered.bytes, source); + assert_lookup_only(&http); + } +} + +#[test] +fn async_standalone_media_requests_only_admitted_missing_variants() { + let signer = async_variant_signer(true); + let media = EncypherMedia::new(Arc::clone(&signer)); + let source = Bytes::from_static(SOURCE); + let sha = sha256_hex(SOURCE); + let reader_url = "https://publisher.example.com/media/image.png"; + for (query, admitted) in [ + ("edition=uk&asset=b", true), + ("edition=uk&edition=us", false), + ("secret=private", false), + ] { + let http = policy_http(); + if admitted { + http.push_response(202, b"{}".to_vec()); + } + let kv = Arc::new(MemoryKv::default()); + let runtime = services(&http, &kv).with_timer(Arc::new(StoppedTimer)); + let missed = block_on(media.sign( + &runtime, + &MediaJob { + reader_query: query, + ..job(reader_url, &source, &sha, None, ORIGIN) + }, + )) + .expect_err("unprepared media must remain unsigned"); + assert_eq!(missed.reason, "not_prepared"); + if admitted { + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + let bodies = http.recorded_request_bodies(); + let signal: crate::preparation::protocol::SignalRequest = + serde_json::from_slice(bodies.last().expect("should submit preparation")) + .expect("should encode the signal"); + assert_eq!(signal.target.path, "/media/image.png"); + assert_eq!(signal.target.variant, "asset=b&edition=uk"); + let identity = crate::preparation::protocol::MediaIdentity { + credential_context: signer.credential_context(), + policy_digest: POLICY_DIGEST, + page_url: reader_url, + variant: &signal.target.variant, + carrier: crate::preparation::protocol::Carrier::External, + mime: "image/png", + source_sha256: &sha, + } + .digest(); + assert_eq!(signal.items[0].identity, identity); + } else { + assert_lookup_only(&http); + } + } +} + +#[test] +fn async_standalone_media_refuses_a_record_for_other_bytes() { + let signer = async_signer(); + let media = EncypherMedia::new(Arc::clone(&signer)); + let source = Bytes::from_static(SOURCE); + let sha = sha256_hex(SOURCE); + let kv = Arc::new(MemoryKv::default()); + publish_record( + &kv, + &standalone_identity(&signer, SOURCE), + &json!({ + "kind": "external", + "source_sha256": sha256_hex(SIGNED), + "headers": [["link", LINK]], + }), + ); + let http = policy_http(); + let refused = block_on(media.sign( + &services(&http, &kv), + &job(READER, &source, &sha, Some(READER), ORIGIN), + )) + .expect_err("should refuse a record binding other bytes"); + assert_eq!(refused.reason, "published_record_invalid"); + assert_lookup_only(&http); +} + +#[test] +fn async_large_media_is_decorated_only_for_its_exact_published_version() { + let signer = async_signer(); + let media = EncypherMedia::new(Arc::clone(&signer)); + let digest = sha256_hex(b"every byte of the version"); + let locator = |etag: &'static str, length: u64| LargeLookupJob { + reader_url: "https://publisher.example.com/media/ferry.mp4", + reader_query: "", + origin_identity: "origin", + source_origin_host: "origin.example.com", + content_type: "video/mp4", + etag, + length, + }; + let origin_identity_sha256 = sha256_hex(b"origin"); + let identity = crate::preparation::protocol::LargeLocator { + credential_context: signer.credential_context(), + policy_digest: POLICY_DIGEST, + reader_url: "https://publisher.example.com/media/ferry.mp4", + variant: "", + origin_host: "origin.example.com", + origin_identity_sha256: &origin_identity_sha256, + mime: "video/mp4", + etag: "\"v1\"", + length: 20_000_000, + } + .digest(); + let kv = Arc::new(MemoryKv::default()); + publish_record( + &kv, + &identity, + &json!({ + "kind": "large", + "sha256": digest, + "length": 20_000_000_u64, + "headers": [["link", LINK]], + }), + ); + + let http = policy_http(); + let published = + block_on(media.lookup_large(&services(&http, &kv), &locator("\"v1\"", 20_000_000))) + .expect("should find the version's record"); + assert_eq!(published.length, 20_000_000); + assert_eq!( + published + .sha256 + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(), + digest, + "should carry the whole-source digest verified at publication" + ); + assert_lookup_only(&http); + + for (etag, length, reason) in [ + ("\"v2\"", 20_000_000, "not_prepared"), + ("\"v1\"", 20_000_001, "not_prepared"), + ] { + let http = policy_http(); + let failure = block_on(media.lookup_large(&services(&http, &kv), &locator(etag, length))) + .expect_err("another version should find nothing"); + assert_eq!(failure.reason, reason, "{etag} {length}"); + assert_lookup_only(&http); + } +} + +#[test] +fn async_large_media_never_reuses_a_publication_after_its_configured_source_changes() { + let signer = async_signer(); + let media = EncypherMedia::new(Arc::clone(&signer)); + let source_a = b"library A's ferry video"; + let length = u64::try_from(source_a.len()).expect("should fit source length"); + // Core's configured-origin identities for one route before and after an + // operator remaps it to another folder on the same origin host. The + // public URL, fetched host, MIME, strong ETag and length all stay equal: + // validators are scoped to each resource, not to the configuration. + let identity_a = "https://origin.example.com/library-a\0/media/"; + let identity_b = "https://origin.example.com/library-b\0/media/"; + let job_a = LargeLookupJob { + reader_url: "https://publisher.example.com/media/ferry.mp4", + reader_query: "", + origin_identity: identity_a, + source_origin_host: "origin.example.com", + content_type: "video/mp4", + etag: "\"revision-1\"", + length, + }; + let job_b = LargeLookupJob { + origin_identity: identity_b, + ..job_a + }; + // A's record as publication stored it for configured source A, whose + // identity core's authenticated facts reported as this digest. + let published_a = crate::preparation::protocol::LargeLocator { + credential_context: signer.credential_context(), + policy_digest: POLICY_DIGEST, + reader_url: job_a.reader_url, + variant: "", + origin_host: job_a.source_origin_host, + origin_identity_sha256: &sha256_hex(identity_a.as_bytes()), + mime: job_a.content_type, + etag: job_a.etag, + length, + } + .digest(); + let kv = Arc::new(MemoryKv::default()); + publish_record( + &kv, + &published_a, + &json!({ + "kind": "large", + "sha256": sha256_hex(source_a), + "length": length, + "headers": [["link", LINK]], + }), + ); + + let http = policy_http(); + let published = block_on(media.lookup_large(&services(&http, &kv), &job_a)) + .expect("should reuse A's publication while the route still serves source A"); + let digest: [u8; 32] = Sha256::digest(source_a).into(); + assert_eq!(published.sha256, digest, "should carry A's verified digest"); + assert_lookup_only(&http); + + let http = policy_http(); + let failure = block_on(media.lookup_large(&services(&http, &kv), &job_b)) + .expect_err("should not attach A's manifest and digest to remapped source B"); + assert_eq!(failure.reason, "not_prepared"); + assert_lookup_only(&http); +} + +#[test] +fn async_large_media_never_reuses_a_query_free_publication_for_another_resource() { + let signer = async_variant_signer(false); + let media = EncypherMedia::new(Arc::clone(&signer)); + let source_a = b"query-free resource A"; + let source_b = b"query-pick resource B"; + assert_eq!(source_a.len(), source_b.len()); + assert_ne!(source_a, source_b); + let length = u64::try_from(source_a.len()).expect("should fit source length"); + let reader_url = "https://publisher.example.com/media/ferry.mp4"; + let identity = crate::preparation::protocol::LargeLocator { + credential_context: signer.credential_context(), + policy_digest: POLICY_DIGEST, + reader_url, + variant: "", + origin_host: "origin.example.com", + origin_identity_sha256: &sha256_hex(b"origin"), + mime: "video/mp4", + etag: "\"revision-1\"", + length, + } + .digest(); + let kv = Arc::new(MemoryKv::default()); + publish_record( + &kv, + &identity, + &json!({ + "kind": "large", + "sha256": sha256_hex(source_a), + "length": length, + "headers": [["link", LINK]], + }), + ); + // The reader selected B with ?asset=b. Its origin is allowed to use the + // same strong revision tag as A: validators are scoped to each resource. + let job = LargeLookupJob { + reader_url, + reader_query: "asset=b", + origin_identity: "origin", + source_origin_host: "origin.example.com", + content_type: "video/mp4", + etag: "\"revision-1\"", + length, + }; + let http = policy_http(); + let failure = block_on(media.lookup_large(&services(&http, &kv), &job)) + .expect_err("should not attach A's manifest and digest to query-selected B"); + assert_eq!(failure.reason, "not_prepared"); + assert_lookup_only(&http); +} + +#[test] +fn async_large_media_reuses_only_its_admitted_variant_and_refuses_unsupported_queries() { + let signer = async_variant_signer(true); + let media = EncypherMedia::new(Arc::clone(&signer)); + let source_a = b"query-free resource A"; + let source_b = b"query-pick resource B"; + assert_eq!(source_a.len(), source_b.len()); + let length = u64::try_from(source_a.len()).expect("should fit source length"); + let job = LargeLookupJob { + reader_url: "https://publisher.example.com/media/ferry.mp4", + reader_query: "", + origin_identity: "origin", + source_origin_host: "origin.example.com", + content_type: "video/mp4", + etag: "\"revision-1\"", + length, + }; + let kv = Arc::new(MemoryKv::default()); + let link_b = "; rel=\"c2pa-manifest\""; + for (variant, source, link) in [ + ("", source_a, LINK), + ("asset=b&edition=uk", source_b, link_b), + ] { + let identity = crate::preparation::protocol::LargeLocator { + credential_context: signer.credential_context(), + policy_digest: POLICY_DIGEST, + reader_url: job.reader_url, + variant, + origin_host: job.source_origin_host, + origin_identity_sha256: &sha256_hex(job.origin_identity.as_bytes()), + mime: job.content_type, + etag: job.etag, + length, + } + .digest(); + publish_record( + &kv, + &identity, + &json!({ + "kind": "large", + "sha256": sha256_hex(source), + "length": length, + "headers": [["link", link]], + }), + ); + } + for (query, source, link) in [ + ("", source_a, LINK), + ("asset=b&edition=uk", source_b, link_b), + ] { + let http = policy_http(); + let published = block_on(media.lookup_large( + &services(&http, &kv), + &LargeLookupJob { + reader_query: query, + ..job + }, + )) + .expect("should reuse this resource's own publication"); + let digest: [u8; 32] = Sha256::digest(source).into(); + assert_eq!(published.sha256, digest, "{query}"); + assert_eq!(published.length, length); + assert!( + published + .headers + .iter() + .any(|(name, value)| { name == "link" && value.to_str().ok() == Some(link) }), + "{query}: should select this resource's own manifest" + ); + assert_lookup_only(&http); + } + for query in [ + // Raw query order and encoding can select different origin resources. + // Equal per-resource validators cannot establish alias equivalence. + "edition=uk&asset=b", + "asset=%62&edition=uk", + "unknown=b", + "asset=c", + "asset=b&asset=b", + "asset=b&asset=c", + "asset=b&%61sset=b", + "asset=b&edition=unknown", + ] { + let http = policy_http(); + let runtime = services(&http, &kv).with_timer(Arc::new(StoppedTimer)); + let failure = block_on(media.lookup_large( + &runtime, + &LargeLookupJob { + reader_query: query, + ..job + }, + )) + .expect_err("should refuse an unsupported query rather than fall back to A or B"); + assert_eq!(failure.reason, "not_prepared", "{query}"); + assert!( + http.recorded_request_uris().is_empty(), + "{query}: should neither check policy nor hint an unadmitted target" + ); + } +} + +#[test] +fn async_large_media_hints_the_exact_admitted_variant_on_a_miss() { + let signer = async_variant_signer(true); + let media = EncypherMedia::new(Arc::clone(&signer)); + let job = LargeLookupJob { + reader_url: "https://publisher.example.com/media/ferry.mp4", + reader_query: "asset=b&edition=uk", + origin_identity: "origin", + source_origin_host: "origin.example.com", + content_type: "video/mp4", + etag: "\"revision-1\"", + length: 20_000_000, + }; + let identity = crate::preparation::protocol::LargeLocator { + credential_context: signer.credential_context(), + policy_digest: POLICY_DIGEST, + reader_url: job.reader_url, + variant: "asset=b&edition=uk", + origin_host: job.source_origin_host, + origin_identity_sha256: &sha256_hex(job.origin_identity.as_bytes()), + mime: job.content_type, + etag: job.etag, + length: job.length, + } + .digest(); + let http = policy_http(); + http.push_response(202, b"{}".to_vec()); + let kv = Arc::new(MemoryKv::default()); + let runtime = services(&http, &kv).with_timer(Arc::new(StoppedTimer)); + let failure = block_on(media.lookup_large(&runtime, &job)) + .expect_err("should leave the unprepared variant unsigned"); + assert_eq!(failure.reason, "not_prepared"); + assert_eq!(http.recorded_request_methods(), ["GET", "POST"]); + let bodies = http.recorded_request_bodies(); + let signal: crate::preparation::protocol::SignalRequest = + serde_json::from_slice(bodies.last().expect("should send a hint")) + .expect("should encode the signal protocol"); + assert_eq!(signal.target.path, "/media/ferry.mp4"); + assert_eq!(signal.target.variant, "asset=b&edition=uk"); + assert_eq!(signal.items.len(), 1); + assert_eq!(signal.items[0].identity, identity); + assert_eq!( + signal.items[0].kind, + crate::preparation::protocol::ItemKind::Large + ); +} + +#[test] +fn only_a_granted_snapshot_read_is_observed_and_its_facts_are_authenticated() { + let signer = async_signer(); + let media = EncypherMedia::new(Arc::clone(&signer)); + let mut request = Request::builder() + .uri(READER) + .body(EdgeBody::empty()) + .expect("should build"); + assert!( + !media.observes_snapshot(&IntegrationRequestState::of(&request)), + "an ordinary reader's request is never a snapshot" + ); + let nonce = "0123456789abcdef0123456789abcdef"; + IntegrationRequestState::observe( + &mut request, + crate::INTEGRATION_ID, + crate::preparation::edge::SnapshotGrant { + nonce: nonce.to_owned(), + }, + ); + let state = IntegrationRequestState::of(&request); + assert!(media.observes_snapshot(&state)); + assert!( + !EncypherMedia::new(signer_synchronous()).observes_snapshot(&state), + "the synchronous mode observes nothing" + ); + + let origin_identity_sha256 = sha256_hex(b"https://origin.example.com\0/media/"); + let facts = SnapshotFacts { + status: StatusCode::OK, + private: true, + no_transform: false, + manifest_link: false, + content_encoding: String::new(), + origin_host: "origin.example.com".to_owned(), + origin_identity_sha256: origin_identity_sha256.clone(), + source_reference: Some("https://origin.example.com/a.png".to_owned()), + }; + let headers = media.snapshot_headers(&state, &facts); + let (name, value) = headers.first().expect("should write the facts header"); + assert_eq!(name.as_str(), crate::preparation::protocol::FACTS_HEADER); + let reported = crate::preparation::protocol::verify_facts_header( + "publication-secret-for-tests", + nonce, + value.to_str().expect("should be text"), + ) + .expect("should verify for the grant's nonce"); + assert!(reported.private, "should report the origin's own privacy"); + assert_eq!(reported.origin_host, "origin.example.com"); + assert_eq!( + reported.origin_identity_sha256, origin_identity_sha256, + "should authenticate core's configured-source digest with the facts" + ); + assert!( + crate::preparation::protocol::verify_facts_header( + "publication-secret-for-tests", + "fedcba9876543210fedcba9876543210", + value.to_str().expect("should be text"), + ) + .is_none(), + "should verify for no other read" + ); +} + +fn signer_synchronous() -> Arc { + signer() +} diff --git a/crates/provenance/encypher/src/processor/prepared.rs b/crates/provenance/encypher/src/processor/prepared.rs new file mode 100644 index 0000000000..8aa58ad3ad --- /dev/null +++ b/crates/provenance/encypher/src/processor/prepared.rs @@ -0,0 +1,1087 @@ +//! Large media signed where it is delivered, through the prepared lane. +//! +//! A source too large to capture is never uploaded and never held whole. +//! Core pins one version of it by its strong `ETag` and hashes it in bounded +//! chunks. This lane then asks the Encypher prepared-signing endpoints for a +//! remote manifest over those exact bytes: +//! +//! 1. The policy, then the lane's capabilities for the asset. +//! 2. The signed probe release the service advertises, verified against the +//! pinned release key before it runs. +//! 3. The probe's disclosure of the source's container structure, read from +//! the pinned version. A source that already carries an embedded C2PA +//! manifest stops here: a remote manifest cannot stand beside it, as an +//! embedded manifest wins over a `Link`, so the source is delivered as it +//! is, with its own credentials, reported as existing provenance kept and +//! never as newly signed or verified. +//! 4. `prepare`, then the probe's check that the server's plan covers the +//! whole source unchanged, then a last check that the version is still +//! current, then `finalize`. +//! 5. The returned store is checked to bind the whole-source digest. The +//! reader gets the origin's bytes with a `Link` to that store. +//! +//! Every failure is a refusal with a code. Core then delivers the origin's +//! bytes unsigned; nothing is published for a source the lane did not +//! verify. + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use bytes::Bytes; +use edgezero_core::body::Body as EdgeBody; +use http::{HeaderValue, Method, Request, StatusCode, header}; +use serde::Serialize; +use serde_json::{Value, json}; +use std::time::Duration; +use trusted_server_core::media_signing::{ + LargeMediaJob, LargeSignedMedia, MediaSourceError, VersionedMediaSource, +}; +use trusted_server_core::middleware::ResponseHeader; +use trusted_server_core::platform::RuntimeServices; + +use super::{ + Deadline, Reason, SKIP_WRITE_RESERVE, Signer, WORKER_TOKEN_HEADER, is_excluded_prefix, +}; +use crate::api::valid_policy_digest; +use crate::media_api::{ + self, IDEMPOTENCY_KEY_HEADER, canonical_media_type, safe_source_reference, sha256_hex, + valid_origin_host, +}; +use crate::prepared_probe::{ + ProbeCallError, ProbeDescriptor, ProbeLimits, ProbeModule, ProbeSource, ReleaseAuthority, +}; + +const CAPABILITIES_PATH: &str = "/api/v1/public/cdn/media/hash/capabilities"; +const PREPARE_PATH: &str = "/api/v1/public/cdn/media/hash/prepare"; +const FINALIZE_PATH: &str = "/api/v1/public/cdn/media/hash/finalize"; +/// Signed probe releases, served without credentials and never changed. +const PROBE_PATH: &str = "/api/v1/"; +const PROTOCOL: &str = "encypher-prepared-signing/2"; +/// The cached artifact of a probe release, by its SHA-256. +const PROBE_KEY_PREFIX: &str = "encypher:probe:v1"; +/// The largest probe artifact accepted. +const MAX_PROBE_BYTES: usize = 4 * 1024 * 1024; +/// A capabilities answer is a few descriptors and limits. +const MAX_CAPABILITIES_BYTES: usize = 64 * 1024; +/// A prepare answer is ids and the planner's geometry. +const MAX_PREPARE_BYTES: usize = 1024 * 1024; +/// A finalize answer carries the manifest store, base64 encoded, besides +/// ids and URLs. +const FINALIZE_OVERHEAD_BYTES: usize = 64 * 1024; +/// The largest finalized manifest store the lane takes, whatever the +/// service allows: the answer, its decoded store and the probe's copy of it +/// must fit an edge instance. An external manifest of an unsigned source, +/// with no ingredient chain or identity assertion, is far smaller. +const MAX_FINALIZED_MANIFEST_BYTES: usize = 2 * 1024 * 1024; +/// The refusal for a finalized manifest larger than the lane takes. +const MANIFEST_TOO_LARGE: &str = "manifest_too_large"; +/// The outcome of a source that already carries an embedded C2PA manifest: +/// delivered as it is, its own provenance kept, nothing newly signed. +const EMBEDDED_PROVENANCE: &str = "source_has_embedded_provenance"; +/// The outcome of a source whose XMP already names a C2PA manifest: delivered +/// as it is, its own provenance kept, nothing newly signed. +const REFERENCED_PROVENANCE: &str = "source_has_provenance_reference"; + +const INSTANCE_ID_HEADER: &str = "x-encypher-manifest-instance-id"; +const DOCUMENT_ID_HEADER: &str = "x-encypher-document-id"; +const VERIFICATION_URL_HEADER: &str = "x-encypher-verification-url"; + +/// Why the prepared lane did not sign a large source. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum LargeRefusal { + /// A refusal the module shares with its other lanes. + Module(Reason), + /// The pinned source could not be read as the version it was pinned at. + Source(MediaSourceError), + /// A refusal particular to this lane. + Lane(&'static str), +} + +impl LargeRefusal { + pub(super) fn code(self) -> &'static str { + match self { + Self::Module(reason) => reason.code(), + Self::Source(error) => error.code(), + Self::Lane(code) => code, + } + } + + /// Whether the source was delivered with its own embedded credentials, + /// its provenance kept rather than lost to a failure. + pub(super) fn retains_provenance(self) -> bool { + self == Self::Lane(EMBEDDED_PROVENANCE) || self == Self::Lane(REFERENCED_PROVENANCE) + } +} + +impl From for LargeRefusal { + fn from(reason: Reason) -> Self { + Self::Module(reason) + } +} + +impl From for LargeRefusal { + fn from(error: MediaSourceError) -> Self { + Self::Source(error) + } +} + +/// The pinned version, as the probe reads it. +struct PinnedSource<'a>(&'a VersionedMediaSource<'a>); + +impl ProbeSource for PinnedSource<'_> { + type Error = MediaSourceError; + + fn size(&self) -> u64 { + self.0.size() + } + + async fn read_exact_at( + &self, + offset: u64, + destination: &mut [u8], + ) -> Result<(), MediaSourceError> { + self.0.read_exact_at(offset, destination).await + } +} + +/// The lane's capabilities for one asset, as the service answers them. +struct Capabilities { + policy_digest: String, + formats: Vec, + total_size_bytes: u64, + manifest_decoded_bytes: usize, + probes: Vec, +} + +impl Capabilities { + /// The largest finalized store the lane takes: the service's maximum, + /// never more than the edge's own. + fn manifest_limit(&self) -> usize { + self.manifest_decoded_bytes + .min(MAX_FINALIZED_MANIFEST_BYTES) + } + + fn parse(body: &[u8]) -> Option { + let value: Value = serde_json::from_slice(body).ok()?; + if value.get("protocol")?.as_str()? != PROTOCOL || !value.get("enabled")?.as_bool()? { + return None; + } + let maximums = value.get("maximums")?; + Some(Self { + policy_digest: value.get("policy_digest")?.as_str()?.to_owned(), + formats: value + .get("formats")? + .as_array()? + .iter() + .filter_map(Value::as_str) + .map(str::to_owned) + .collect(), + total_size_bytes: maximums.get("total_size_bytes")?.as_u64()?, + manifest_decoded_bytes: usize::try_from( + maximums.get("manifest_decoded_bytes")?.as_u64()?, + ) + .ok()?, + probes: ProbeDescriptor::parse_list(value.get("probe")), + }) + } +} + +/// What the lane signs one source with, once the gates passed. +#[derive(Clone, Copy)] +struct Lane<'a> { + job: &'a LargeMediaJob<'a>, + reference: &'a str, + mime: &'a str, + policy_digest: &'a str, + /// The release key probes are held to: production, except in tests. + authority: &'a ReleaseAuthority, +} + +/// The prepare request, serialized from borrowed values so the disclosure +/// is not copied. +#[derive(Serialize)] +struct PrepareBody<'a> { + page_url: &'a str, + expected_policy_digest: &'a str, + protocol: &'a str, + probe: ProbeSelection<'a>, + source: PreparedSource<'a>, + structure: Vec>, +} + +#[derive(Serialize)] +struct ProbeSelection<'a> { + sha256: &'a str, + planner_id: &'a str, +} + +#[derive(Serialize)] +struct PreparedSource<'a> { + reference: &'a str, + origin_host: &'a str, + mime: &'a str, + sha256: &'a str, + byte_length: u64, +} + +#[derive(Serialize)] +struct StructureChunk<'a> { + offset: u64, + b64: &'a str, +} + +/// The verified finalize answer: what the delivered headers name. +struct Finalized { + manifest_url: String, + instance_id: String, + document_id: String, + verification_url: String, +} + +/// Whether `value` is an identifier safe in a response header. +fn valid_identifier(value: &str) -> bool { + !value.is_empty() && value.len() <= 256 && value.bytes().all(|byte| byte.is_ascii_graphic()) +} + +/// Whether `value` is a canonical absolute `https` URL safe inside a `Link`. +fn valid_https_url(value: &str) -> bool { + value.len() <= 2048 + && value + .bytes() + .all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'<' | b'>' | b'"')) + && url::Url::parse(value).is_ok_and(|parsed| { + parsed.scheme() == "https" && parsed.host_str().is_some() && parsed.as_str() == value + }) +} + +/// The reason a probe call failed, logged with the probe's own code. +fn probe_refusal>(op: &str, error: ProbeCallError) -> LargeRefusal { + log::info!("encypher: large media probe op={op} code={}", error.code()); + match error { + ProbeCallError::Source(error) => error.into(), + ProbeCallError::Refused(refusal) if refusal.integrity => { + LargeRefusal::Lane("source_provenance_invalid") + } + ProbeCallError::Refused(_) => LargeRefusal::Lane("probe_refused"), + ProbeCallError::Failed(_) => LargeRefusal::Lane("probe_failed"), + } +} + +/// Whether `items` is the one program of an unchanged external plan: the +/// whole source, once, with nothing excluded or inserted. +fn covers_whole_source(items: &[Value], size: u64) -> bool { + matches!(items, [item] if item.get("range").and_then(Value::as_array).is_some_and(|range| { + range.len() == 2 + && range.first().and_then(Value::as_u64) == Some(0) + && range.get(1).and_then(Value::as_u64) == Some(size) + }) && item.as_object().is_some_and(|object| object.len() == 1)) +} + +/// The reason for an error answer of the prepared endpoints, by status and +/// failure code. +fn answer_refusal(status: StatusCode, code: Option<&str>) -> LargeRefusal { + match (status.as_u16(), code) { + (409, Some("TRUSTED_SERVER_POLICY_CHANGED")) => Reason::PolicyChanged.into(), + (409, Some("PROBE_VERSION_UNSUPPORTED" | "PLAN_STALE")) => { + LargeRefusal::Lane("probe_release_stale") + } + (409, Some("SOURCE_DIGEST_MISMATCH")) => LargeRefusal::Lane("source_digest_mismatch"), + (422, Some("SOURCE_PROVENANCE_INVALID")) => LargeRefusal::Lane("source_provenance_invalid"), + // A store the probe's disclosure did not report: kept, not signed. + (422, Some("SOURCE_MANIFEST_PRESENT")) => LargeRefusal::Lane(EMBEDDED_PROVENANCE), + (422, Some("HASH_MODE_UNSUPPORTED")) => LargeRefusal::Lane("format_unsupported"), + (403 | 404, _) => Reason::Forbidden.into(), + (409, _) => LargeRefusal::Lane("prepared_conflict"), + (413, _) => Reason::PayloadTooLarge.into(), + (422, _) => Reason::Unprocessable.into(), + (429, _) => Reason::RateLimited.into(), + (500..=599, _) => Reason::ServerError.into(), + _ => Reason::UnexpectedStatus.into(), + } +} + +impl Signer { + /// Largest source the prepared lane signs; larger media is delivered + /// unsigned. + pub(super) fn max_large_media_bytes(&self) -> u64 { + self.config.max_large_media_bytes + } + + /// The whole time core and this lane may spend on one large source, or + /// none when the lane is off. + pub(super) fn max_large_media_duration(&self) -> Duration { + if self.config.max_large_media_bytes == 0 { + Duration::ZERO + } else { + Duration::from_millis(self.config.max_large_media_ms) + } + } + + /// Sign one large source core pinned and hashed, returning the headers + /// every response of it carries and the namespace it was signed under. + /// Probe releases are always held to the pinned production key. + pub(super) async fn sign_large( + &self, + services: &RuntimeServices, + job: &LargeMediaJob<'_>, + ) -> Result { + self.sign_large_with(services, job, &ReleaseAuthority::production()) + .await + } + + /// [`Self::sign_large`] with the release `authority` given, which only + /// tests set to anything but production. + async fn sign_large_with( + &self, + services: &RuntimeServices, + job: &LargeMediaJob<'_>, + authority: &ReleaseAuthority, + ) -> Result { + let source = job.source; + let size = source.size(); + let reader_url = url::Url::parse(job.reader_url).map_err(|_| Reason::OutsideClaim)?; + let reader_host = reader_url.host_str().ok_or(Reason::OutsideClaim)?; + if !matches!(reader_url.scheme(), "https" | "http") + || !self.claims_host(reader_host) + || is_excluded_prefix(reader_url.path()) + { + return Err(Reason::OutsideClaim.into()); + } + if size == 0 || size > self.max_large_media_bytes() { + return Err(Reason::MediaTooLarge.into()); + } + if !valid_origin_host(job.source_origin_host) { + return Err(Reason::InvalidOriginHost.into()); + } + if !valid_policy_digest(job.source_sha256) { + return Err(Reason::SourceDigestMismatch.into()); + } + // The fetched address when it names the origin's bytes; otherwise + // the reader's own address, which names the same bytes here because + // they are delivered unchanged. A source with neither is refused. + let reference = job + .source_reference + .filter(|reference| safe_source_reference(reference, job.source_origin_host)) + .or_else(|| { + job.reader_reference.filter(|reference| { + *reference == job.reader_url && safe_source_reference(reference, reader_host) + }) + }) + .ok_or(LargeRefusal::Lane("source_reference_required"))?; + let mime = canonical_media_type(job.content_type); + + // What is left of the lane's budget after core hashed the source, + // less the reserve a policy eviction may still use. + let mut work = Deadline::new( + services.timer(), + job.remaining.saturating_sub(SKIP_WRITE_RESERVE), + ); + work.start().await; + // Asked live: the namespace core keeps the result under must be the + // policy the service holds now, never a cached one a revocation + // outlived. + let policy_digest = self + .request_policy(job.reader_url, services, &mut work) + .await?; + let lane = Lane { + job, + reference, + mime: &mime, + policy_digest: &policy_digest, + authority, + }; + let result = self.run_prepared(services, &lane, &mut work).await; + if let Err(LargeRefusal::Module(Reason::PolicyChanged | Reason::PolicyDigestMismatch)) = + &result + { + self.evict_policy(services.kv_store(), &mut Self::skip_deadline(services)) + .await; + } + let finalized = result?; + let link = format!("<{}>; rel=\"c2pa-manifest\"", finalized.manifest_url); + let mut headers: Vec = Vec::with_capacity(4); + for (name, value) in [ + (header::LINK, link.as_str()), + ( + http::HeaderName::from_static(INSTANCE_ID_HEADER), + finalized.instance_id.as_str(), + ), + ( + http::HeaderName::from_static(DOCUMENT_ID_HEADER), + finalized.document_id.as_str(), + ), + ( + http::HeaderName::from_static(VERIFICATION_URL_HEADER), + finalized.verification_url.as_str(), + ), + ] { + headers.push(( + name, + HeaderValue::from_str(value).map_err(|_| Reason::InvalidSignedResponse)?, + )); + } + Ok(LargeSignedMedia { + headers, + namespace: self.kept_namespace(&policy_digest), + }) + } + + /// The lane's steps after the gates: capabilities, probe, disclosure, + /// prepare, plan check, version check, finalize, store check. + async fn run_prepared( + &self, + services: &RuntimeServices, + lane: &Lane<'_>, + work: &mut Deadline<'_>, + ) -> Result { + let Lane { + job, + reference, + mime, + policy_digest, + authority, + } = *lane; + let source = job.source; + let size = source.size(); + let capabilities = self.capabilities(services, job.reader_url, work).await?; + if capabilities.policy_digest != policy_digest { + return Err(Reason::PolicyChanged.into()); + } + if !capabilities.formats.iter().any(|format| format == mime) { + return Err(LargeRefusal::Lane("format_unsupported")); + } + if size > capabilities.total_size_bytes { + return Err(Reason::MediaTooLarge.into()); + } + let probe = self + .load_probe(services, authority, &capabilities.probes, work) + .await?; + let pinned = PinnedSource(source); + + let mut extracted = match work + .race(probe.call( + "extract", + &json!({"mime": mime, "total_size": size, "disposition": "external"}), + &pinned, + )) + .await + { + None => return Err(Reason::Timeout.into()), + Some(result) => result.map_err(|error| probe_refusal("extract", error))?, + }; + // An embedded manifest wins over a `Link`, so a remote one would be + // ignored beside it, and a source whose XMP already names a manifest + // has provenance of its own: either way the source keeps it. A + // release that does not say whether the XMP names one cannot be + // trusted to have looked. + if extracted.get("credentials").is_some() { + return Err(LargeRefusal::Lane(EMBEDDED_PROVENANCE)); + } + match extracted + .get("has_provenance_reference") + .and_then(Value::as_bool) + { + Some(true) => return Err(LargeRefusal::Lane(REFERENCED_PROVENANCE)), + Some(false) => {} + None => return Err(LargeRefusal::Lane("probe_result_invalid")), + } + // The disclosure is taken out of the result, not copied: it is sent + // at prepare and handed back to the probe at plan verification. + let disclosure = extracted + .get_mut("chunks") + .map(Value::take) + .ok_or(LargeRefusal::Lane("probe_result_invalid"))?; + drop(extracted); + let structure = disclosure + .as_array() + .ok_or(LargeRefusal::Lane("probe_result_invalid"))? + .iter() + .map(|chunk| { + Some(StructureChunk { + offset: chunk.get("offset")?.as_u64()?, + b64: chunk.get("data")?.as_str()?, + }) + }) + .collect::>>() + .ok_or(LargeRefusal::Lane("probe_result_invalid"))?; + + let idempotency_key = sha256_hex( + format!( + "prepared\n{}\n{policy_digest}\n{}\n{reference}\n{}\n{mime}\n{}\n{size}\n{}", + self.cache_context, + job.reader_url, + job.source_origin_host, + job.source_sha256, + source.version().etag(), + ) + .as_bytes(), + ); + let descriptor = probe.descriptor(); + let prepare = PrepareBody { + page_url: job.reader_url, + expected_policy_digest: policy_digest, + protocol: PROTOCOL, + probe: ProbeSelection { + sha256: &descriptor.sha256, + planner_id: &descriptor.planner_id, + }, + source: PreparedSource { + reference, + origin_host: job.source_origin_host, + mime, + sha256: job.source_sha256, + byte_length: size, + }, + structure, + }; + let mut prepared = self + .post( + services, + PREPARE_PATH, + &prepare, + &idempotency_key, + MAX_PREPARE_BYTES, + work, + ) + .await?; + drop(prepare); + let plan_id = prepared + .get("plan_id") + .and_then(Value::as_str) + .filter(|id| id.len() == 36) + .ok_or(LargeRefusal::Lane("prepare_answer_invalid"))? + .to_owned(); + if prepared.get("policy_digest").and_then(Value::as_str) != Some(policy_digest) { + return Err(Reason::PolicyDigestMismatch.into()); + } + let unchanged = prepared + .get("local_ops") + .and_then(Value::as_array) + .is_some_and(Vec::is_empty) + && prepared.get("effective_action").and_then(Value::as_str) == Some("c2pa.opened"); + let geometry = prepared + .get_mut("geometry") + .map(Value::take) + .filter(|geometry| { + geometry.get("disposition").and_then(Value::as_str) == Some("external") + }); + drop(prepared); + let Some(geometry) = geometry.filter(|_| unchanged) else { + return Err(LargeRefusal::Lane("prepare_answer_invalid")); + }; + + // The probe recomputes the server's plan from the same bytes; an + // unchanged external plan hashes the whole source once. + let verified = match work + .race(probe.call( + "verify_plan", + &json!({ + "mime": mime, + "total_size": size, + "disclosure": disclosure, + "geometry": geometry, + }), + &pinned, + )) + .await + { + None => return Err(Reason::Timeout.into()), + Some(result) => result.map_err(|error| probe_refusal("verify_plan", error))?, + }; + let items = verified + .get("hash_items") + .and_then(Value::as_array) + .ok_or(LargeRefusal::Lane("probe_result_invalid"))?; + if !covers_whole_source(items, size) { + return Err(LargeRefusal::Lane("plan_not_whole_source")); + } + drop(verified); + match work.race(source.confirm_current()).await { + None => return Err(Reason::Timeout.into()), + Some(result) => result?, + } + + let finalize = json!({ + "page_url": job.reader_url, + "expected_policy_digest": policy_digest, + "protocol": PROTOCOL, + "plan_id": plan_id, + "binding": {"type": "data_hash", "digest": job.source_sha256}, + "xmp_reference": false, + }); + let manifest_limit = capabilities.manifest_limit(); + let answer_limit = manifest_limit + .saturating_mul(4) + .div_ceil(3) + .saturating_add(FINALIZE_OVERHEAD_BYTES); + let finalized = self + .post( + services, + FINALIZE_PATH, + &finalize, + &idempotency_key, + answer_limit, + work, + ) + .await + .map_err(|refusal| match refusal { + LargeRefusal::Module(Reason::ResponseTooLarge) => { + LargeRefusal::Lane(MANIFEST_TOO_LARGE) + } + other => other, + })?; + self.verify_finalized(&probe, &finalized, job, policy_digest, &capabilities, work) + .await + } + + /// Checks the finalize answer against the plan and the pinned source, + /// and that the store it returns binds the whole-source digest. + async fn verify_finalized( + &self, + probe: &ProbeModule, + finalized: &Value, + job: &LargeMediaJob<'_>, + policy_digest: &str, + capabilities: &Capabilities, + work: &mut Deadline<'_>, + ) -> Result { + let text = |name: &str| finalized.get(name).and_then(Value::as_str); + if text("policy_digest") != Some(policy_digest) { + return Err(Reason::PolicyDigestMismatch.into()); + } + let external = text("manifest_disposition") == Some("external") + && text("xmp_reference") == Some("declined") + && text("binding_algorithm") == Some("c2pa.hash.data") + && finalized.get("expected_final_size").and_then(Value::as_u64) + == Some(job.source.size()); + if !external { + return Err(LargeRefusal::Lane("finalize_answer_invalid")); + } + let store_b64 = text("manifest_store_b64").ok_or(Reason::UnsignedResponse)?; + let manifest_limit = capabilities.manifest_limit(); + if store_b64.len() + > manifest_limit + .saturating_mul(4) + .div_ceil(3) + .saturating_add(3) + { + return Err(LargeRefusal::Lane(MANIFEST_TOO_LARGE)); + } + let store = STANDARD + .decode(store_b64) + .map_err(|_| Reason::InvalidSignedResponse)?; + if store.is_empty() { + return Err(Reason::InvalidSignedResponse.into()); + } + if store.len() > manifest_limit { + return Err(LargeRefusal::Lane(MANIFEST_TOO_LARGE)); + } + if text("manifest_sha256") != Some(sha256_hex(&store).as_str()) { + return Err(Reason::SignedDigestMismatch.into()); + } + let manifest_url = text("manifest_url") + .filter(|url| valid_https_url(url)) + .ok_or(Reason::InvalidSignedResponse)?; + let verification_url = text("verification_url") + .filter(|url| valid_https_url(url)) + .ok_or(Reason::InvalidSignedResponse)?; + let instance_id = text("instance_id") + .filter(|id| valid_identifier(id)) + .ok_or(Reason::InvalidSignedResponse)?; + let document_id = text("document_id") + .filter(|id| valid_identifier(id)) + .ok_or(Reason::InvalidSignedResponse)?; + // The store's hard binding must be over the whole source, nothing + // excluded, with the digest core computed over the pinned version. + let checked = match work + .race(probe.call_without_source::( + "check_store", + &json!({ + "manifest_store_b64": store_b64, + "digest": job.source_sha256, + "binding_recipe": {"type": "data_hash", "algorithm": "sha256", "exclusions": []}, + }), + )) + .await + { + None => return Err(Reason::Timeout.into()), + Some(result) => result.map_err(|error| probe_refusal("check_store", error))?, + }; + if checked.get("ok").and_then(Value::as_bool) != Some(true) { + return Err(Reason::SignedDigestMismatch.into()); + } + Ok(Finalized { + manifest_url: manifest_url.to_owned(), + instance_id: instance_id.to_owned(), + document_id: document_id.to_owned(), + verification_url: verification_url.to_owned(), + }) + } + + /// The lane's capabilities for the asset at `page_url`. + async fn capabilities( + &self, + services: &RuntimeServices, + page_url: &str, + work: &mut Deadline<'_>, + ) -> Result { + let mut url = url::Url::parse(&format!( + "{}{CAPABILITIES_PATH}", + self.config.api_base_url.trim_end_matches('/') + )) + .map_err(|_| Reason::RequestBuild)?; + url.query_pairs_mut().append_pair("page_url", page_url); + let request = Request::builder() + .method(Method::GET) + .uri(url.as_str()) + .header(WORKER_TOKEN_HEADER, self.worker_token()?) + .body(EdgeBody::empty()) + .map_err(|_| Reason::RequestBuild)?; + let body = self + .exchange(services, request, MAX_CAPABILITIES_BYTES, work) + .await?; + Capabilities::parse(&body).ok_or(LargeRefusal::Lane("capabilities_invalid")) + } + + /// The newest verified release the service advertises, from the cache or + /// fetched, compiled with the edge's limits. + async fn load_probe( + &self, + services: &RuntimeServices, + authority: &ReleaseAuthority, + advertised: &[ProbeDescriptor], + work: &mut Deadline<'_>, + ) -> Result { + let descriptor = authority.select(advertised).map_err(|error| { + log::info!("encypher: large media probe release code={}", error.code()); + LargeRefusal::Lane(error.code()) + })?; + let kv = services.kv_store(); + let key = format!("{PROBE_KEY_PREFIX}:{}", descriptor.sha256); + let cached = match work.race(kv.get_bytes(&key)).await { + Some(Ok(Some(bytes))) if sha256_hex(&bytes) == descriptor.sha256 => Some(bytes), + _ => None, + }; + let artifact = match cached { + Some(bytes) => bytes, + None => { + let request = Request::builder() + .method(Method::GET) + .uri(format!( + "{}{PROBE_PATH}{}", + self.config.api_base_url.trim_end_matches('/'), + descriptor.artifact_path() + )) + .body(EdgeBody::empty()) + .map_err(|_| Reason::RequestBuild)?; + let bytes = self + .exchange(services, request, MAX_PROBE_BYTES, work) + .await?; + if sha256_hex(&bytes) != descriptor.sha256 { + return Err(LargeRefusal::Lane("PROBE_RELEASE_INVALID")); + } + // Releases never change, so a cached copy needs no expiry + // beyond the plan lifetime. + let ttl = Duration::from_secs(self.config.plan_ttl_seconds); + if !matches!( + work.race(kv.put_bytes_with_ttl(&key, bytes.clone(), ttl)) + .await, + Some(Ok(())) + ) { + log::debug!("encypher: probe release cache write failed"); + } + bytes + } + }; + let loaded = match work + // The host's default limits are the edge's: structure only, never + // the payload. A release that runs out of fuel is refused. + .race(ProbeModule::load( + authority, + descriptor, + &artifact, + ProbeLimits::default(), + )) + .await + { + None => return Err(Reason::Timeout.into()), + Some(result) => result, + }; + loaded.map_err(|error| { + log::info!("encypher: large media probe release code={}", error.code()); + LargeRefusal::Lane(error.code()) + }) + } + + /// One authenticated JSON `POST` to a prepared endpoint. + async fn post( + &self, + services: &RuntimeServices, + path: &str, + body: &impl Serialize, + idempotency_key: &str, + max_response_bytes: usize, + work: &mut Deadline<'_>, + ) -> Result { + let body = serde_json::to_vec(body).map_err(|_| Reason::RequestBuild)?; + let request = Request::builder() + .method(Method::POST) + .uri(format!( + "{}{path}", + self.config.api_base_url.trim_end_matches('/') + )) + .header(header::CONTENT_TYPE, "application/json") + .header(WORKER_TOKEN_HEADER, self.worker_token()?) + .header(IDEMPOTENCY_KEY_HEADER, idempotency_key) + .body(EdgeBody::from(body)) + .map_err(|_| Reason::RequestBuild)?; + let answer = self + .exchange(services, request, max_response_bytes, work) + .await?; + serde_json::from_slice(&answer).map_err(|_| Reason::MalformedResponse.into()) + } + + /// One bounded call; a non-`200` answer is a refusal with its code. + async fn exchange( + &self, + services: &RuntimeServices, + request: Request, + max_response_bytes: usize, + work: &mut Deadline<'_>, + ) -> Result { + let limit = max_response_bytes.saturating_add(1); + let response = self.send_api(services, request, limit, work).await?; + let status = response.status(); + if status != StatusCode::OK { + let code = work + .race( + response + .into_body() + .into_bytes_bounded(media_api::MAX_ERROR_BODY_BYTES), + ) + .await + .and_then(Result::ok) + .and_then(|body| media_api::error_code(&body)); + log::info!( + "encypher: large media answer status={} code={}", + status.as_u16(), + code.as_deref().unwrap_or("none") + ); + return Err(answer_refusal(status, code.as_deref())); + } + let body = match work + .race(response.into_body().into_bytes_bounded(limit)) + .await + { + None => return Err(Reason::Timeout.into()), + Some(Err(_)) => return Err(Reason::ResponseTooLarge.into()), + Some(Ok(body)) => body, + }; + if body.len() > max_response_bytes { + return Err(Reason::ResponseTooLarge.into()); + } + Ok(body) + } +} + +#[cfg(test)] +mod flow_tests; + +#[cfg(test)] +mod tests { + use super::*; + + use std::sync::Arc; + + use futures::executor::block_on; + use http::HeaderMap; + use trusted_server_core::media_signing::SourceVersion; + use trusted_server_core::platform::PlatformHttpClient; + use trusted_server_core::platform::test_support::{ + StubHttpClient, build_services_with_http_client, + }; + + use crate::EncypherConfig; + + const SHA: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn signer() -> Signer { + let config: EncypherConfig = serde_json::from_value(json!({ + "api_base_url": "https://api.example.com", + "domain": "publisher.example.com", + "worker_token": "cwk_test_worker_token_value", + "max_large_media_bytes": 1_073_741_824_u64, + })) + .expect("should parse config"); + Signer::new(config, 5_000_000) + } + + /// The gate refusal for one job, made before any request is sent. + fn refusal( + reader_url: &str, + source_reference: Option<&str>, + reader_reference: Option<&str>, + origin_host: &str, + ) -> &'static str { + let http = Arc::new(StubHttpClient::new()); + let services = + build_services_with_http_client(Arc::clone(&http) as Arc); + let origin = url::Url::parse("https://origin.example.com/video/ferry.mp4") + .expect("should parse the origin URL"); + let headers = HeaderMap::new(); + let source = VersionedMediaSource::new( + &services, + &origin, + &headers, + "origin", + SourceVersion::new("\"v1\"", 64 * 1024 * 1024).expect("should pin a strong ETag"), + ); + let job = LargeMediaJob { + reader_url, + content_type: "video/mp4", + source: &source, + source_sha256: SHA, + source_reference, + reader_reference, + source_origin_host: origin_host, + remaining: Duration::from_secs(60), + }; + let code = block_on(signer().sign_large(&services, &job)) + .expect_err("should refuse at the gates") + .code(); + assert!( + http.recorded_request_methods().is_empty(), + "should refuse before any request" + ); + code + } + + #[test] + fn a_large_source_without_a_usable_reference_is_refused_before_any_request() { + let reader = "https://publisher.example.com/video/ferry.mp4"; + assert_eq!( + refusal( + "https://cdn.example.org/video/ferry.mp4", + None, + None, + "origin.example.com" + ), + "outside_claim" + ); + assert_eq!( + refusal(reader, None, None, "Origin.example.com:8080"), + "invalid_origin_host" + ); + // A fetched address on another host than the one fetched, and a + // reader address that is not the delivered one, name no source. + assert_eq!( + refusal( + reader, + Some("https://elsewhere.example.com/video/ferry.mp4"), + Some("https://publisher.example.com/video/other.mp4"), + "origin.example.com", + ), + "source_reference_required" + ); + assert_eq!( + refusal(reader, None, None, "origin.example.com"), + "source_reference_required" + ); + } + + #[test] + fn only_a_single_whole_source_range_is_an_unchanged_plan() { + assert!(covers_whole_source(&[json!({"range": [0, 100]})], 100)); + for items in [ + vec![json!({"range": [0, 99]})], + vec![json!({"range": [0, 50]}), json!({"range": [50, 50]})], + vec![json!({"range": [0, 100]}), json!({"literal": "QQ=="})], + vec![json!({"range": [0, 100], "skip": true})], + vec![], + ] { + assert!( + !covers_whole_source(&items, 100), + "should refuse {items:?} as an unchanged plan" + ); + } + } + + #[test] + fn a_source_with_its_own_manifest_is_reported_as_kept_not_failed_or_signed() { + let kept = answer_refusal( + StatusCode::UNPROCESSABLE_ENTITY, + Some("SOURCE_MANIFEST_PRESENT"), + ); + assert_eq!(kept.code(), "source_has_embedded_provenance"); + assert!( + kept.retains_provenance(), + "should report the provenance kept" + ); + for failed in [ + answer_refusal( + StatusCode::UNPROCESSABLE_ENTITY, + Some("HASH_MODE_UNSUPPORTED"), + ), + answer_refusal( + StatusCode::UNPROCESSABLE_ENTITY, + Some("SOURCE_PROVENANCE_INVALID"), + ), + LargeRefusal::Module(Reason::Timeout), + ] { + assert!( + !failed.retains_provenance(), + "{}: should not claim kept provenance", + failed.code() + ); + } + } + + #[test] + fn prepared_errors_map_to_specific_refusals() { + assert_eq!( + answer_refusal(StatusCode::CONFLICT, Some("TRUSTED_SERVER_POLICY_CHANGED")), + LargeRefusal::Module(Reason::PolicyChanged) + ); + assert_eq!( + answer_refusal(StatusCode::CONFLICT, Some("PROBE_VERSION_UNSUPPORTED")).code(), + "probe_release_stale" + ); + assert_eq!( + answer_refusal( + StatusCode::UNPROCESSABLE_ENTITY, + Some("SOURCE_PROVENANCE_INVALID") + ) + .code(), + "source_provenance_invalid" + ); + assert_eq!( + answer_refusal(StatusCode::FORBIDDEN, Some("claim_changed")), + LargeRefusal::Module(Reason::Forbidden) + ); + } + + #[test] + fn capabilities_parse_only_an_enabled_v2_lane() { + let body = |enabled: bool, protocol: &str| { + serde_json::to_vec(&json!({ + "protocol": protocol, + "enabled": enabled, + "policy_digest": "a".repeat(64), + "formats": ["video/mp4"], + "maximums": { + "total_size_bytes": 1_000_000_000_u64, + "structure_decoded_bytes": 33_554_432, + "structure_chunks": 8192, + "manifest_decoded_bytes": 1_048_576, + "prepare_request_bytes": 50_331_648, + }, + "probe": [], + })) + .expect("should encode") + }; + let parsed = Capabilities::parse(&body(true, PROTOCOL)).expect("should parse"); + assert_eq!(parsed.formats, ["video/mp4"]); + assert_eq!(parsed.manifest_decoded_bytes, 1_048_576); + assert!(Capabilities::parse(&body(false, PROTOCOL)).is_none()); + assert!(Capabilities::parse(&body(true, "encypher-prepared-signing/1")).is_none()); + } +} diff --git a/crates/provenance/encypher/src/processor/prepared/flow_tests.rs b/crates/provenance/encypher/src/processor/prepared/flow_tests.rs new file mode 100644 index 0000000000..8ae75e4d41 --- /dev/null +++ b/crates/provenance/encypher/src/processor/prepared/flow_tests.rs @@ -0,0 +1,581 @@ +//! The prepared lane end to end, against a stand-in probe release signed with +//! a test key, a stub service and a stub origin. +//! +//! The stand-in has no format logic: each operation answers JSON the test +//! chose, so these tests exercise the lane's own orchestration and checks, +//! and no Encypher planner code enters this repository. Production always +//! holds releases to the pinned production key; only these tests pass +//! another authority. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use bytes::Bytes; +use ed25519_dalek::{Signer as _, SigningKey}; +use edgezero_core::key_value_store::KvPage; +use futures::executor::block_on; +use http::HeaderMap; +use serde_json::{Value, json}; +use trusted_server_core::media_signing::{ + LargeMediaJob, LargeSignedMedia, SourceVersion, VersionedMediaSource, +}; +use trusted_server_core::platform::test_support::{ + StubHttpClient, build_services_with_http_client, +}; +use trusted_server_core::platform::{ + KvError, PlatformHttpClient, PlatformKvStore, RuntimeServices, +}; + +use super::{LargeRefusal, Signer}; +use crate::EncypherConfig; +use crate::media_api::sha256_hex; +use crate::prepared_probe::{ProbeDescriptor, ReleaseAuthority}; + +const POLICY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const OTHER_POLICY: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const SOURCE_SHA: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const SIZE: u64 = 20_000_000; +const READER_URL: &str = "https://publisher.example.com/video/ferry.mp4"; +const ORIGIN_HOST: &str = "publisher.example.com"; +const PLAN_ID: &str = "0f8fad5b-d9cb-469f-a165-70867728950e"; +const STORE: &[u8] = b"jumb-c2pa-remote-manifest-store"; + +const STAND_IN: &str = r#" +(module + (import "env" "host_read" (func $host_read (param i64 i32 i32) (result i32))) + (memory (export "memory") 2) + (global $heap (mut i32) (i32.const 16384)) + (global $result_ptr (mut i32) (i32.const 0)) + (global $result_len (mut i32) (i32.const 0)) + (data (i32.const 1024) "@VERSION@") + (data (i32.const 2048) "@EXTRACT@") + (data (i32.const 4096) "@VERIFY@") + (data (i32.const 6144) "@CHECK@") + (data (i32.const 8192) "@UNKNOWN@") + (func (export "probe_abi") (result i32) (i32.const 1)) + (func (export "probe_alloc") (param $length i32) (result i32) + (local $pointer i32) + (local.set $pointer (global.get $heap)) + (global.set $heap (i32.add (global.get $heap) (local.get $length))) + (local.get $pointer)) + (func (export "probe_free") (param i32 i32)) + (func (export "probe_result_ptr") (result i32) (global.get $result_ptr)) + (func (export "probe_result_len") (result i32) (global.get $result_len)) + (func $answer (param $pointer i32) (param $length i32) (param $code i32) (result i32) + (global.set $result_ptr (local.get $pointer)) + (global.set $result_len (local.get $length)) + (local.get $code)) + (func (export "probe_call") + (param $op i32) (param $op_len i32) (param $arg i32) (param $arg_len i32) (result i32) + (local $kind i32) + (local.set $kind (i32.load8_u (local.get $op))) + ;; "version" (7 bytes) and "verify_plan" (11 bytes) both start with v + (if (i32.and (i32.eq (local.get $kind) (i32.const 118)) (i32.eq (local.get $op_len) (i32.const 7))) + (then (return (call $answer (i32.const 1024) (i32.const @VERSION_LEN@) (i32.const 0))))) + (if (i32.eq (local.get $kind) (i32.const 118)) + (then (return (call $answer (i32.const 4096) (i32.const @VERIFY_LEN@) (i32.const 0))))) + ;; "extract" + (if (i32.eq (local.get $kind) (i32.const 101)) + (then (return (call $answer (i32.const 2048) (i32.const @EXTRACT_LEN@) (i32.const 0))))) + ;; "check_store" + (if (i32.eq (local.get $kind) (i32.const 99)) + (then (return (call $answer (i32.const 6144) (i32.const @CHECK_LEN@) (i32.const 0))))) + (call $answer (i32.const 8192) (i32.const @UNKNOWN_LEN@) (i32.const 1))) +) +"#; + +/// What each stand-in operation answers. +struct Answers { + extract: Value, + verify: Value, + check: Value, +} + +impl Default for Answers { + fn default() -> Self { + Self { + extract: json!({ + "chunks": [{"offset": 0, "data": "AAAA"}], + "has_provenance_reference": false, + }), + verify: json!({"hash_items": [{"range": [0, SIZE]}]}), + check: json!({"ok": true}), + } + } +} + +fn planner() -> String { + "d".repeat(64) +} + +fn wat_string(text: &str) -> String { + text.replace('\\', "\\\\").replace('"', "\\\"") +} + +/// The stand-in release answering `answers`. +fn release(answers: &Answers) -> Vec { + let version = json!({"planner_id": planner()}).to_string(); + let unknown = + json!({"error": {"code": "UNKNOWN_OP", "integrity": false, "message": "no such op"}}) + .to_string(); + let extract = answers.extract.to_string(); + let verify = answers.verify.to_string(); + let check = answers.check.to_string(); + let text = STAND_IN + .replace("@VERSION@", &wat_string(&version)) + .replace("@VERSION_LEN@", &version.len().to_string()) + .replace("@EXTRACT@", &wat_string(&extract)) + .replace("@EXTRACT_LEN@", &extract.len().to_string()) + .replace("@VERIFY@", &wat_string(&verify)) + .replace("@VERIFY_LEN@", &verify.len().to_string()) + .replace("@CHECK@", &wat_string(&check)) + .replace("@CHECK_LEN@", &check.len().to_string()) + .replace("@UNKNOWN@", &wat_string(&unknown)) + .replace("@UNKNOWN_LEN@", &unknown.len().to_string()); + wat::parse_str(&text).expect("should assemble the stand-in release") +} + +fn release_key() -> SigningKey { + SigningKey::from_bytes(&[11; 32]) +} + +/// The descriptor of `artifact`, signed with the test release key. +fn descriptor(artifact: &[u8]) -> ProbeDescriptor { + let mut descriptor = ProbeDescriptor { + abi: 1, + planner_id: planner(), + release: 3, + sha256: sha256_hex(artifact), + signature: String::new(), + }; + let signature = release_key().sign(descriptor.canonical_json().as_bytes()); + descriptor.signature = STANDARD.encode(signature.to_bytes()); + descriptor +} + +fn test_authority() -> ReleaseAuthority { + ReleaseAuthority::pinned(vec![release_key().verifying_key()], 1, Vec::new()) +} + +/// A runtime KV store held in memory. +#[derive(Default)] +struct MemoryKv(Mutex>); + +impl MemoryKv { + fn has(&self, key: &str) -> bool { + self.0.lock().expect("should lock").contains_key(key) + } +} + +#[async_trait::async_trait(?Send)] +impl PlatformKvStore for MemoryKv { + async fn get_bytes(&self, key: &str) -> Result, KvError> { + Ok(self.0.lock().expect("should lock").get(key).cloned()) + } + + async fn put_bytes(&self, key: &str, value: Bytes) -> Result<(), KvError> { + self.0 + .lock() + .expect("should lock") + .insert(key.to_owned(), value); + Ok(()) + } + + async fn put_bytes_with_ttl( + &self, + key: &str, + value: Bytes, + _ttl: std::time::Duration, + ) -> Result<(), KvError> { + self.put_bytes(key, value).await + } + + async fn delete(&self, key: &str) -> Result<(), KvError> { + self.0.lock().expect("should lock").remove(key); + Ok(()) + } + + async fn list_keys_page( + &self, + _prefix: &str, + _cursor: Option<&str>, + _limit: usize, + ) -> Result { + Ok(KvPage::default()) + } +} + +fn signer() -> Signer { + let config: EncypherConfig = serde_json::from_value(json!({ + "api_base_url": "https://api.example.com", + "domain": "publisher.example.com", + "worker_token": "cwk_test_worker_token_value", + "max_large_media_bytes": 1_073_741_824_u64, + })) + .expect("should parse config"); + Signer::new(config, 5_000_000) +} + +fn finalize_answer() -> Value { + json!({ + "protocol": "encypher-prepared-signing/2", + "plan_id": PLAN_ID, + "instance_id": "xmp:iid:0001", + "document_id": "doc-1", + "manifest_disposition": "external", + "content_hash_kind": "hard_binding", + "binding_algorithm": "c2pa.hash.data", + "manifest_store_b64": STANDARD.encode(STORE), + "manifest_sha256": sha256_hex(STORE), + "manifest_url": "https://api.example.com/manifests/doc-1", + "xmp_reference": "declined", + "expected_final_size": SIZE, + "verification_url": "https://verify.example.com/doc-1", + "reused": false, + "policy_digest": POLICY, + }) +} + +fn prepare_answer() -> Value { + json!({ + "protocol": "encypher-prepared-signing/2", + "plan_id": PLAN_ID, + "instance_id": "xmp:iid:0001", + "document_id": "doc-1", + "mime": "video/mp4", + "effective_action": "c2pa.opened", + "geometry": {"disposition": "external"}, + "local_ops": [], + "policy_digest": POLICY, + }) +} + +/// One run of the lane, and what it asked of the service and origin. +struct Run { + result: Result, + http: Arc, + kv: Arc, + signer: Signer, +} + +impl Run { + fn methods(&self) -> Vec { + self.http.recorded_request_methods() + } + + fn refusal(&self) -> LargeRefusal { + *self.result.as_ref().expect_err("should refuse") + } + + fn body(&self, index: usize) -> Value { + serde_json::from_slice(&self.http.recorded_request_bodies()[index]) + .expect("should send JSON") + } +} + +/// The scenario's answers, queued in the order the lane asks for them. +struct Scenario { + answers: Answers, + capabilities_policy: &'static str, + /// The largest manifest store the service says it returns. + manifest_decoded_bytes: usize, + prepare: Value, + finalize: Value, +} + +impl Default for Scenario { + fn default() -> Self { + Self { + answers: Answers::default(), + capabilities_policy: POLICY, + manifest_decoded_bytes: 1_048_576, + prepare: prepare_answer(), + finalize: finalize_answer(), + } + } +} + +fn run(scenario: &Scenario) -> Run { + let artifact = release(&scenario.answers); + let probe = descriptor(&artifact); + let http = Arc::new(StubHttpClient::new()); + http.set_streaming_responses_supported(true); + http.push_response( + 200, + serde_json::to_vec(&json!({"policy_digest": POLICY})).expect("JSON"), + ); + http.push_response( + 200, + serde_json::to_vec(&json!({ + "protocol": "encypher-prepared-signing/2", + "enabled": true, + "policy_digest": scenario.capabilities_policy, + "formats": ["video/mp4"], + "maximums": { + "total_size_bytes": 9_007_199_246_352_383_u64, + "structure_decoded_bytes": 33_554_432, + "structure_chunks": 8192, + "manifest_decoded_bytes": scenario.manifest_decoded_bytes, + "prepare_request_bytes": 50_331_648, + }, + "probe": [probe.to_json()], + })) + .expect("JSON"), + ); + http.push_response(200, artifact); + http.push_response(200, serde_json::to_vec(&scenario.prepare).expect("JSON")); + // The pinned one-byte read that confirms the version before finalize. + http.push_response_with_headers( + 206, + b"\x00".to_vec(), + vec![ + ("content-range", format!("bytes 0-0/{SIZE}")), + ("content-length", "1".to_owned()), + ("etag", "\"v1\"".to_owned()), + ], + ); + http.push_response(200, serde_json::to_vec(&scenario.finalize).expect("JSON")); + + let kv = Arc::new(MemoryKv::default()); + let services: RuntimeServices = + build_services_with_http_client(Arc::clone(&http) as Arc) + .with_kv_store(Arc::clone(&kv) as Arc); + let origin = url::Url::parse("https://publisher.example.com/video/ferry.mp4") + .expect("should parse the origin URL"); + let headers = HeaderMap::new(); + let source = VersionedMediaSource::new( + &services, + &origin, + &headers, + "origin", + SourceVersion::new("\"v1\"", SIZE).expect("should pin a strong ETag"), + ); + let job = LargeMediaJob { + reader_url: READER_URL, + content_type: "video/mp4", + source: &source, + source_sha256: SOURCE_SHA, + source_reference: Some(READER_URL), + reader_reference: None, + source_origin_host: ORIGIN_HOST, + remaining: std::time::Duration::from_secs(60), + }; + let signer = signer(); + let result = block_on(signer.sign_large_with(&services, &job, &test_authority())); + Run { + result, + http, + kv, + signer, + } +} + +#[test] +fn an_unchanged_source_is_bound_whole_after_its_version_is_confirmed() { + let run = run(&Scenario::default()); + + run.result.as_ref().expect("should sign"); + assert_eq!( + run.methods(), + ["GET", "GET", "GET", "POST", "GET", "POST"], + "should confirm the pinned version after prepare and before finalize" + ); + let finalize = run.body(5); + assert_eq!( + finalize["binding"], + json!({"type": "data_hash", "digest": SOURCE_SHA}), + "should bind the whole-source digest" + ); + assert_eq!( + finalize["xmp_reference"], false, + "should never edit the source" + ); + let keys: Vec = run + .http + .recorded_request_headers() + .iter() + .filter_map(|headers| { + headers + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case("idempotency-key")) + .map(|(_, value)| value.clone()) + }) + .collect(); + assert_eq!(keys.len(), 2, "should key prepare and finalize"); + assert_eq!(keys[0], keys[1], "should finalize under the plan's own key"); +} + +#[test] +fn a_manifest_larger_than_the_lane_takes_is_refused() { + let run = run(&Scenario { + manifest_decoded_bytes: STORE.len() - 1, + ..Scenario::default() + }); + assert_eq!( + run.refusal().code(), + "manifest_too_large", + "should hold the store to the smaller of the service's and the edge's limit" + ); +} + +#[test] +fn a_source_with_provenance_of_its_own_keeps_it_and_is_never_prepared() { + for (extract, reason) in [ + ( + json!({ + "chunks": [{"offset": 0, "data": "AAAA"}], + "credentials": {"manifests": 1}, + "has_provenance_reference": false, + }), + "source_has_embedded_provenance", + ), + ( + json!({ + "chunks": [{"offset": 0, "data": "AAAA"}], + "has_provenance_reference": true, + }), + "source_has_provenance_reference", + ), + ] { + let run = run(&Scenario { + answers: Answers { + extract, + ..Answers::default() + }, + ..Scenario::default() + }); + + let refusal = run.refusal(); + assert_eq!(refusal.code(), reason); + assert!(refusal.retains_provenance(), "{reason}: should keep it"); + assert!( + !run.methods().iter().any(|method| method == "POST"), + "{reason}: should never prepare or finalize" + ); + } +} + +#[test] +fn a_release_that_does_not_report_a_provenance_reference_is_not_trusted() { + for flag in [None, Some(json!("false")), Some(json!(0))] { + let mut extract = json!({"chunks": [{"offset": 0, "data": "AAAA"}]}); + if let Some(flag) = flag.clone() { + extract["has_provenance_reference"] = flag; + } + let run = run(&Scenario { + answers: Answers { + extract, + ..Answers::default() + }, + ..Scenario::default() + }); + + let refusal = run.refusal(); + assert_eq!(refusal.code(), "probe_result_invalid", "{flag:?}"); + assert!(!refusal.retains_provenance(), "{flag:?}: should not claim"); + assert!( + !run.methods().iter().any(|method| method == "POST"), + "{flag:?}: should never prepare" + ); + } +} + +#[test] +fn a_plan_that_is_not_the_unchanged_whole_source_is_never_finalized() { + let mut edits = prepare_answer(); + edits["local_ops"] = json!([{"op": "xmp"}]); + let mut edited = prepare_answer(); + edited["effective_action"] = json!("c2pa.edited"); + let mut embedded = prepare_answer(); + embedded["geometry"] = json!({"disposition": "embedded"}); + for prepare in [edits, edited, embedded] { + let run = run(&Scenario { + prepare, + ..Scenario::default() + }); + assert_eq!(run.refusal().code(), "prepare_answer_invalid"); + assert_eq!(run.methods().len(), 4, "should stop after prepare"); + } + + let run = run(&Scenario { + answers: Answers { + verify: json!({"hash_items": [{"range": [0, SIZE - 1]}]}), + ..Answers::default() + }, + ..Scenario::default() + }); + assert_eq!(run.refusal().code(), "plan_not_whole_source"); + assert_eq!( + run.methods().len(), + 4, + "should stop before the version check" + ); +} + +#[test] +fn a_finalized_answer_that_does_not_bind_the_source_is_never_delivered() { + let mut resized = finalize_answer(); + resized["expected_final_size"] = json!(SIZE + 1); + let mut swapped = finalize_answer(); + swapped["manifest_sha256"] = json!(sha256_hex(b"another store")); + let mut embedded = finalize_answer(); + embedded["manifest_disposition"] = json!("embedded"); + for (finalize, reason) in [ + (resized, "finalize_answer_invalid"), + (swapped, "signed_digest_mismatch"), + (embedded, "finalize_answer_invalid"), + ] { + let run = run(&Scenario { + finalize, + ..Scenario::default() + }); + assert_eq!(run.refusal().code(), reason); + } + + let run = run(&Scenario { + answers: Answers { + check: json!({"ok": false}), + ..Answers::default() + }, + ..Scenario::default() + }); + assert_eq!( + run.refusal().code(), + "signed_digest_mismatch", + "should refuse a store that does not bind the whole-source digest" + ); +} + +#[test] +fn a_changed_policy_evicts_the_cached_policy_and_publishes_nothing() { + let changed = run(&Scenario { + capabilities_policy: OTHER_POLICY, + ..Scenario::default() + }); + assert_eq!(changed.refusal().code(), "policy_changed"); + assert!( + !changed.kv.has(&changed.signer.policy_key), + "should evict the policy the lane held" + ); + assert!(!changed.methods().iter().any(|method| method == "POST")); + + let mut prepared = prepare_answer(); + prepared["policy_digest"] = json!(OTHER_POLICY); + let mismatched = run(&Scenario { + prepare: prepared, + ..Scenario::default() + }); + assert_eq!(mismatched.refusal().code(), "policy_digest_mismatch"); + assert!(!mismatched.kv.has(&mismatched.signer.policy_key)); + + let mut finalized = finalize_answer(); + finalized["policy_digest"] = json!(OTHER_POLICY); + let late = run(&Scenario { + finalize: finalized, + ..Scenario::default() + }); + assert_eq!(late.refusal().code(), "policy_digest_mismatch"); + assert!(!late.kv.has(&late.signer.policy_key)); +} diff --git a/crates/provenance/encypher/src/processor/published.rs b/crates/provenance/encypher/src/processor/published.rs new file mode 100644 index 0000000000..0c895242e8 --- /dev/null +++ b/crates/provenance/encypher/src/processor/published.rs @@ -0,0 +1,663 @@ +//! Reader delivery in the asynchronous prepared mode: published results +//! only, never a signing call. +//! +//! Each item's identity is computed from the exact projection or bytes this +//! reader is about to receive, through the same functions the preparation +//! worker runs on its snapshot. A result is used only when the delivery +//! instance published one under exactly that identity and no retirement +//! marker stands for it; a marker that cannot be read refuses the result. +//! The lookup is a plain read of the runtime KV store: it never starts or +//! joins a loader, and never waits for preparation. +//! +//! Every item without a result is delivered as it is, unsigned, and on a +//! deployment with `reader_trigger` the worker is handed one bounded hint: +//! within `admission_ms` on a platform with a timer, or after the response +//! on a platform that declared that lifetime. A hint is acknowledged only +//! when the worker answers that it committed it; it is never durable on +//! the edge, and publication events, warming and reconciliation stay the +//! recovery path for a hint that was lost. + +use std::time::Duration; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use bytes::Bytes; +use futures::future::{Either, select}; +use http::{HeaderName, HeaderValue, Method, Request, StatusCode, header}; +use trusted_server_core::integrations::ensure_integration_backend_with_timeout; +use trusted_server_core::media_signing::{LargeLookupJob, MediaJob, PublishedLarge, SignedMedia}; +use trusted_server_core::middleware::ResponseHeader; +use trusted_server_core::platform::{ + AfterResponseRequest, KvError, PlatformHttpRequest, PlatformKvStore, RuntimeServices, + hand_over_after_response, platform_offers_after_response_handoff, +}; + +use super::{ + Article, Deadline, Reason, SKIP_WRITE_RESERVE, Signer, Surface, Target, embedded, + is_excluded_prefix, log_outcome, render, render_reason, +}; +use crate::INTEGRATION_ID; +use crate::api; +use crate::media_api::{canonical_media_type, sha256_hex, valid_origin_host}; +use crate::preparation::items::{ + DocumentKind, EmbeddedItem, ItemContext, embedded_edit, embedded_items, text_identity, +}; +use crate::preparation::protocol::{ + Carrier, ItemKind, LargeLocator, MediaIdentity, ObservedItem, PREPARATION_PROTOCOL, + PublishedRecord, PublishedResult, SIGNALS_PATH, SignalRequest, TOKEN_HEADER, Target as Goal, + marker_key, result_key, +}; +use crate::preparation::{Execution, PreparationConfig}; +use crate::text; + +/// What one lookup found. +enum Lookup { + /// A published result for exactly the identity. + Found(PublishedResult), + /// No result: the item is unprepared. + Missing, +} + +impl Signer { + /// The module's settings. + pub(crate) fn config(&self) -> &crate::EncypherConfig { + &self.config + } + + /// The host prepared addresses name, see + /// [`PreparationConfig::public_host`]. + pub(crate) fn public_host(&self) -> &str { + self.preparation() + .map_or(self.config.domain.as_str(), |preparation| { + preparation.public_host.as_str() + }) + } + + /// Whether the module runs in the asynchronous prepared mode. + pub(crate) fn is_async(&self) -> bool { + self.config.execution == Execution::AsyncPrepared && self.config.preparation.is_some() + } + + /// The asynchronous mode's settings. + pub(crate) fn preparation(&self) -> Option<&PreparationConfig> { + self.config + .preparation + .as_ref() + .filter(|_| self.config.execution == Execution::AsyncPrepared) + } + + /// The credential context identities bind. + pub(crate) fn credential_context(&self) -> &str { + &self.cache_context + } + + /// The published result for `identity`, after its retirement marker is + /// checked: present refuses it, unreadable refuses it. + async fn published( + &self, + kv: &dyn PlatformKvStore, + identity: &str, + work: &mut Deadline<'_>, + ) -> Result { + match work.race(kv.get_bytes(&marker_key(identity))).await { + None => return Err(Reason::Timeout), + Some(Err(KvError::Unavailable)) => return Err(Reason::KvUnavailable), + Some(Err(_)) => return Err(Reason::MarkerUnavailable), + Some(Ok(Some(_))) => return Err(Reason::Retired), + Some(Ok(None)) => {} + } + let bytes = match work.race(kv.get_bytes(&result_key(identity))).await { + None => return Err(Reason::Timeout), + Some(Err(KvError::Unavailable)) => return Err(Reason::KvUnavailable), + Some(Err(_)) => return Err(Reason::KvError), + Some(Ok(None)) => return Ok(Lookup::Missing), + Some(Ok(Some(bytes))) => bytes, + }; + let record: PublishedRecord = + serde_json::from_slice(&bytes).map_err(|_| Reason::PublishedRecordInvalid)?; + if record.protocol != PREPARATION_PROTOCOL || record.identity != identity { + return Err(Reason::PublishedRecordInvalid); + } + Ok(Lookup::Found(record.result)) + } + + /// The admitted variant of a reader's `query`, or `None` where it names + /// anything the schema does not admit. + fn admitted_variant(&self, query: &str) -> Option { + self.preparation() + .and_then(|preparation| preparation.variants.admit(query)) + } + + /// Looks up every item of a held document and applies what was + /// published: the article's plan first, then embedded media, each by + /// its own identity, so one unprepared item leaves its siblings signed. + /// Returns the bytes to emit. + pub(super) async fn resolve_published_document( + &self, + target: &Target<'_>, + services: &RuntimeServices, + document: &str, + article: Option
, + media: embedded::Scan, + work: &mut Deadline<'_>, + ) -> Vec { + let request = target.request; + let kv = services.kv_store(); + let Some(page_url) = self.canonical_page_url(&request.page_url) else { + return document.as_bytes().to_vec(); + }; + let variant = self.admitted_variant(&request.query); + // A query outside the schema is looked up as the plain address: + // every identity binds the exact text or bytes, so a result found + // is exactly this content's. Only a hint needs an admitted variant. + let lookup_variant = variant.as_deref().unwrap_or_default(); + let mut misses: Vec = Vec::new(); + let mut output = document.to_owned(); + + if let Some(article) = &article { + let policy_digest = match self.policy_digest(&page_url, services, kv, work).await { + Ok(policy_digest) => Some(policy_digest), + Err(reason) => { + log_outcome(false, reason); + None + } + }; + if let Some(policy_digest) = policy_digest { + let context = self.item_context(&page_url, &policy_digest, lookup_variant); + let title = text::title_from_range(document, article.title_range); + let identity = text_identity(&context, article, title.as_deref()); + match self.published(kv, &identity, work).await { + Ok(Lookup::Found(PublishedResult::Text { + plan, + manifest_url, + scalars, + })) if scalars == article.scalars + && api::validate_plan(&plan, manifest_url.as_deref(), article.scalars) + .is_ok() => + { + match render( + document, + article, + &plan, + manifest_url.as_deref(), + self.max_document_bytes, + ) { + Ok(signed) => { + log_outcome(true, Reason::CacheHit); + output = signed; + } + Err(error) => log_outcome(false, render_reason(error)), + } + } + Ok(Lookup::Found(_)) => log_outcome(false, Reason::PublishedRecordInvalid), + Ok(Lookup::Missing) => { + log_outcome(false, Reason::NotPrepared); + misses.push(ObservedItem { + kind: ItemKind::Text, + identity, + }); + } + Err(reason) => log_outcome(false, reason), + } + } + } + + if !media.is_empty() { + // Media is looked up only under the policy the service answers + // now, as in the synchronous mode, so a revoked credential stops + // every published copy. + match self.request_policy(&page_url, services, work).await { + Ok(policy_digest) => { + let context = self.item_context(&page_url, &policy_digest, lookup_variant); + let kind = match target.surface { + Surface::Page => DocumentKind::Page, + Surface::Stylesheet => DocumentKind::Stylesheet, + }; + let (items, refused) = embedded_items(&context, &output, kind, target.policies); + for reason in refused { + log::info!("encypher: media outcome=unsigned reason={reason}"); + } + let mut edits = Vec::new(); + for item in &items { + match self.published_embedded(kv, &output, item, work).await { + Ok(Some(edit)) => { + log::info!("encypher: media outcome=signed reason=cache_hit"); + edits.push(edit); + } + Ok(None) => { + log::info!("encypher: media outcome=unsigned reason=not_prepared"); + misses.push(ObservedItem { + kind: ItemKind::Embedded, + identity: item.identity.clone(), + }); + } + Err(reason) => log::info!( + "encypher: media outcome=unsigned reason={}", + reason.code() + ), + } + } + if !edits.is_empty() { + match embedded::splice(&output, &edits, self.max_document_bytes) { + Some(spliced) => output = spliced, + None => log::info!( + "encypher: media outcome=unsigned reason=output_too_large" + ), + } + } + } + Err(reason) => super::media::log_unsigned(&media, reason), + } + } + + self.hint(services, &page_url, variant.as_deref(), &misses) + .await; + output.into_bytes() + } + + /// The address identities name for a reader's `page_url` on a claimed + /// host: the configured public host and the path, `https`, without the + /// query. `None` for an address outside the claim. + pub(crate) fn canonical_page_url(&self, page_url: &str) -> Option { + let parsed = url::Url::parse(page_url).ok()?; + let host = parsed.host_str()?; + if !self.claims_host(host) { + return None; + } + Some(format!("https://{}{}", self.public_host(), parsed.path())) + } + + /// What every identity of a document at `page_url` binds. + fn item_context<'a>( + &'a self, + page_url: &'a str, + policy_digest: &'a str, + variant: &'a str, + ) -> ItemContext<'a> { + ItemContext { + credential_context: &self.cache_context, + policy_digest, + page_url, + variant, + article_selector: &self.config.article_selector, + min_text_length: self.config.min_text_length, + max_media_bytes: self.config.max_media_bytes, + } + } + + /// The edit an embedded item's published output makes, `None` where + /// none is published. The output must be exactly the one its record + /// names, made from exactly this item's source. + async fn published_embedded( + &self, + kv: &dyn PlatformKvStore, + document: &str, + item: &EmbeddedItem, + work: &mut Deadline<'_>, + ) -> Result, Reason> { + match self.published(kv, &item.identity, work).await? { + Lookup::Missing => Ok(None), + Lookup::Found(PublishedResult::Embedded { + source_sha256, + output_sha256, + content_type, + output_b64, + .. + }) => { + if source_sha256 != item.sha256 || content_type != item.mime { + return Err(Reason::PublishedRecordInvalid); + } + let output = STANDARD + .decode(output_b64) + .map_err(|_| Reason::PublishedRecordInvalid)?; + if sha256_hex(&output) != output_sha256 { + return Err(Reason::PublishedRecordInvalid); + } + if item.mime == "image/svg+xml" { + embedded::verify_svg_carrier(&item.source, &output)?; + } + embedded_edit(document, item, &output) + .map(Some) + .ok_or(Reason::PublishedRecordInvalid) + } + Lookup::Found(_) => Err(Reason::PublishedRecordInvalid), + } + } + + /// The published representation of one captured standalone item: the + /// origin's own bytes, which the record binds by digest, with the + /// manifest `Link` and ids it names. Never a signing call. + pub(super) async fn published_delivery( + &self, + services: &RuntimeServices, + job: &MediaJob<'_>, + ) -> Result { + let reader_url = url::Url::parse(job.reader_url).map_err(|_| Reason::OutsideClaim)?; + let host = reader_url.host_str().ok_or(Reason::OutsideClaim)?; + if !matches!(reader_url.scheme(), "https" | "http") + || !self.claims_host(host) + || is_excluded_prefix(reader_url.path()) + { + return Err(Reason::OutsideClaim); + } + let page_url = self + .canonical_page_url(job.reader_url) + .ok_or(Reason::OutsideClaim)?; + if job.optimizer_output { + return Err(Reason::OptimizerOutputUnsupported); + } + if job.source.len() > self.config.max_media_bytes { + return Err(Reason::MediaTooLarge); + } + let sha256 = sha256_hex(job.source); + if sha256 != job.source_sha256 { + return Err(Reason::SourceDigestMismatch); + } + let variant = self.admitted_variant(job.reader_query); + let mut work = Deadline::new( + services.timer(), + Duration::from_millis(self.config.max_hold_ms).saturating_sub(SKIP_WRITE_RESERVE), + ); + work.start().await; + let policy_digest = self.request_policy(&page_url, services, &mut work).await?; + let mime = canonical_media_type(job.content_type); + let identity = MediaIdentity { + credential_context: &self.cache_context, + policy_digest: &policy_digest, + page_url: &page_url, + variant: variant.as_deref().unwrap_or_default(), + carrier: Carrier::External, + mime: &mime, + source_sha256: &sha256, + } + .digest(); + match self + .published(services.kv_store(), &identity, &mut work) + .await? + { + Lookup::Found(PublishedResult::External { + source_sha256, + headers, + }) if source_sha256 == sha256 => Ok(SignedMedia { + bytes: job.source.clone(), + content_type: HeaderValue::from_str(job.content_type) + .map_err(|_| Reason::InvalidSignedResponse)?, + headers: response_headers(&headers)?, + namespace: self.kept_namespace(&policy_digest), + }), + Lookup::Found(_) => Err(Reason::PublishedRecordInvalid), + Lookup::Missing => { + self.hint( + services, + &page_url, + variant.as_deref(), + &[ObservedItem { + kind: ItemKind::Media, + identity, + }], + ) + .await; + Err(Reason::NotPrepared) + } + } + } + + /// The published headers of one large version, by its validator + /// locator. The record carries the whole-source digest the worker and + /// the delivery instance verified for that version. + pub(super) async fn published_large( + &self, + services: &RuntimeServices, + job: &LargeLookupJob<'_>, + ) -> Result { + let reader_url = url::Url::parse(job.reader_url).map_err(|_| Reason::OutsideClaim)?; + let host = reader_url.host_str().ok_or(Reason::OutsideClaim)?; + if !self.claims_host(host) || is_excluded_prefix(reader_url.path()) { + return Err(Reason::OutsideClaim); + } + let page_url = self + .canonical_page_url(job.reader_url) + .ok_or(Reason::OutsideClaim)?; + let variant = self + .admitted_variant(job.reader_query) + .ok_or(Reason::NotPrepared)?; + // The origin saw the raw query. Ordering and encoding aliases need + // not name the same resource, even when their strong ETags match. + if variant != job.reader_query { + return Err(Reason::NotPrepared); + } + // Core names the configured source as well as its host: the same + // host can serve another source under equal validators. + if !valid_origin_host(job.source_origin_host) || job.origin_identity.is_empty() { + return Err(Reason::InvalidOriginHost); + } + let mut work = Deadline::new( + services.timer(), + Duration::from_millis(self.config.max_hold_ms).saturating_sub(SKIP_WRITE_RESERVE), + ); + work.start().await; + let policy_digest = self.request_policy(&page_url, services, &mut work).await?; + let mime = canonical_media_type(job.content_type); + let origin_identity_sha256 = sha256_hex(job.origin_identity.as_bytes()); + let identity = LargeLocator { + credential_context: &self.cache_context, + policy_digest: &policy_digest, + reader_url: &page_url, + variant: &variant, + origin_host: job.source_origin_host, + origin_identity_sha256: &origin_identity_sha256, + mime: &mime, + etag: job.etag, + length: job.length, + } + .digest(); + match self + .published(services.kv_store(), &identity, &mut work) + .await? + { + Lookup::Found(PublishedResult::Large { + sha256, + length, + headers, + }) => { + let digest = decode_digest(&sha256).ok_or(Reason::PublishedRecordInvalid)?; + if job.length != 0 && length != job.length { + return Err(Reason::PublishedRecordInvalid); + } + Ok(PublishedLarge { + headers: response_headers(&headers)?, + sha256: digest, + length, + }) + } + Lookup::Found(_) => Err(Reason::PublishedRecordInvalid), + Lookup::Missing => { + self.hint( + services, + &page_url, + Some(&variant), + &[ObservedItem { + kind: ItemKind::Large, + identity, + }], + ) + .await; + Err(Reason::NotPrepared) + } + } + } + + /// Hands the worker one bounded hint about `items` of the target at + /// `page_url`, where the deployment asked for reader hints and the + /// target is a configured route with an admitted variant. Never awaited + /// past `admission_ms`, never retried, never a signing call. + async fn hint( + &self, + services: &RuntimeServices, + page_url: &str, + variant: Option<&str>, + items: &[ObservedItem], + ) { + let Some(preparation) = self.preparation().filter(|config| config.reader_trigger) else { + return; + }; + if items.is_empty() { + return; + } + let Some(variant) = variant else { + log::info!("encypher: hint outcome=not_sent reason=variant_not_admitted"); + return; + }; + let Some(path) = url::Url::parse(page_url) + .ok() + .map(|url| url.path().to_owned()) + .filter(|path| preparation.admits_path(path)) + else { + log::info!("encypher: hint outcome=not_sent reason=route_not_configured"); + return; + }; + let target = Goal { + path, + variant: variant.to_owned(), + }; + if !target.is_well_formed() { + log::info!("encypher: hint outcome=not_sent reason=target_invalid"); + return; + } + let Some(body) = SignalRequest::encode_bounded(&preparation.deployment_id, target, items) + else { + log::info!("encypher: hint outcome=not_sent reason=hint_too_large"); + return; + }; + let budget = Duration::from_millis(preparation.admission_ms); + let Ok(backend) = ensure_integration_backend_with_timeout( + services, + &preparation.worker_url, + INTEGRATION_ID, + budget, + ) else { + log::info!("encypher: hint outcome=not_sent reason=backend"); + return; + }; + let Some(request) = signal_request(preparation, Bytes::from(body)) else { + log::info!("encypher: hint outcome=not_sent reason=request_build"); + return; + }; + if let Some(timer) = services.timer() { + let (parts, body) = request.into_parts(); + let request = Request::from_parts(parts, edgezero_core::body::Body::from_bytes(body)); + let send = services.http_client().send( + PlatformHttpRequest::new(request, backend) + .with_cache_bypass() + .with_max_response_body_bytes(1024), + ); + let outcome = match select(Box::pin(send), timer.sleep(budget)).await { + Either::Left((Ok(answer), _)) => match answer.response.status() { + StatusCode::ACCEPTED | StatusCode::OK => "admitted", + StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => { + "refused_retryable" + } + _ => "refused", + }, + Either::Left((Err(_), _)) => "unavailable", + // No acknowledgement within the budget: the hint may or may + // not have been committed, and a later identical one is safe. + Either::Right(_) => "admission_unknown", + }; + log::info!("encypher: hint outcome={outcome} items={}", items.len()); + } else if platform_offers_after_response_handoff() { + let handed = hand_over_after_response(AfterResponseRequest { + request, + backend_name: backend, + }); + log::info!( + "encypher: hint outcome={} items={}", + if handed { + "handed_over_after_response" + } else { + "handoff_full" + }, + items.len() + ); + } else { + log::info!("encypher: hint outcome=not_sent reason=reader_trigger_unavailable"); + } + } +} + +/// The hint request, authenticated with the reader-signal scope only. +fn signal_request(preparation: &PreparationConfig, body: Bytes) -> Option> { + let mut token = HeaderValue::from_str(preparation.signal_secret.expose().trim()).ok()?; + token.set_sensitive(true); + Request::builder() + .method(Method::POST) + .uri(format!( + "{}{SIGNALS_PATH}", + preparation.worker_url.trim_end_matches('/') + )) + .header(header::CONTENT_TYPE, "application/json") + .header(TOKEN_HEADER, token) + .body(body) + .ok() +} + +/// A record's headers as response headers, refusing any that does not +/// parse. +fn response_headers(headers: &[(String, String)]) -> Result, Reason> { + headers + .iter() + .map(|(name, value)| { + Ok(( + HeaderName::from_bytes(name.as_bytes()) + .map_err(|_| Reason::PublishedRecordInvalid)?, + HeaderValue::from_str(value).map_err(|_| Reason::PublishedRecordInvalid)?, + )) + }) + .collect() +} + +/// A lower case hex SHA-256 as its 32 bytes. +fn decode_digest(hex: &str) -> Option<[u8; 32]> { + if !crate::preparation::protocol::is_digest(hex) { + return None; + } + let mut digest = [0_u8; 32]; + for (index, slot) in digest.iter_mut().enumerate() { + *slot = u8::from_str_radix(hex.get(index * 2..index * 2 + 2)?, 16).ok()?; + } + Some(digest) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn published_digest_and_headers_refuse_anything_malformed() { + let hex = "ab".repeat(32); + assert_eq!( + decode_digest(&hex), + Some([0xab; 32]), + "should decode a lower case digest" + ); + assert!( + decode_digest(&"AB".repeat(32)).is_none(), + "should refuse upper case" + ); + assert!( + decode_digest("abc").is_none(), + "should refuse a short digest" + ); + let headers = response_headers(&[( + "link".to_owned(), + "; rel=\"c2pa-manifest\"".to_owned(), + )]) + .expect("should parse a valid header"); + assert_eq!(headers.len(), 1); + assert_eq!( + response_headers(&[("bad name".to_owned(), "v".to_owned())]), + Err(Reason::PublishedRecordInvalid), + "a record with a header that does not parse is refused whole" + ); + } +} diff --git a/crates/provenance/encypher/src/svg_equivalence.rs b/crates/provenance/encypher/src/svg_equivalence.rs new file mode 100644 index 0000000000..3f4a9d0685 --- /dev/null +++ b/crates/provenance/encypher/src/svg_equivalence.rs @@ -0,0 +1,977 @@ +//! The standalone SVG document an inline `` is signed as. +//! +//! A browser builds an inline SVG's tree with the HTML parser, which reads +//! it differently from an XML parser: it needs no namespace declaration, +//! keeps the first of two attributes whose names differ only in case, and +//! decodes HTML character references. What is signed must be a standalone +//! SVG document whose XML tree is the tree the browser built, so a verifier +//! reading the signed bytes sees what the reader saw. +//! +//! The fragment is parsed as HTML in a `body` context, into scraper's +//! namespace-aware tree, as the browser parsed it, and refused unless that +//! yields exactly one `` root with nothing displaced beside it but +//! whitespace. Where the original bytes already parse as XML to the same +//! tree, they are signed unchanged. Otherwise, unless the fragment carries +//! provenance of its own, which is never rewritten, the browser's tree is +//! serialized as XML and kept only when that serialization parses to the +//! same tree both as HTML and as XML. +//! +//! Trees are compared by element namespace and local name, the set of +//! ordinary attributes by namespace, local name and value, text with +//! adjacent text merged and CDATA read as text, and comments. Namespace +//! declarations are not ordinary attributes and are not compared: only the +//! namespaces they resolve are. Anything else, a processing instruction or a +//! doctype, makes two trees unequal. XML is always parsed with DTDs +//! refused, so no entity is ever declared or resolved. + +use std::borrow::Cow; +use std::marker::PhantomData; + +use ego_tree::NodeRef; +use ego_tree::iter::Children; +use html5ever::tendril::TendrilSink as _; +use html5ever::{LocalName, Namespace, ParseOpts, QualName, parse_fragment}; +use scraper::node::Element; +use scraper::{HtmlTreeSink, Node}; + +use crate::embedded; +use crate::processor::Reason; + +const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; +const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; +const XLINK_NAMESPACE: &str = "http://www.w3.org/1999/xlink"; +const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace"; +const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/"; + +/// The most nodes an inline SVG may hold. +const MAX_NODES: usize = 10_000; +/// The deepest an inline SVG's elements may nest. +const MAX_DEPTH: usize = 256; +/// Nodes the HTML fragment parser makes besides the fragment's own: the +/// fragment root, the context element and the root `html` element. +const PARSER_NODES: usize = 3; + +/// The declarations the serialization's root carries. +const SVG_DECLARATION: &str = " xmlns=\"http://www.w3.org/2000/svg\""; +const XLINK_DECLARATION: &str = " xmlns:xlink=\"http://www.w3.org/1999/xlink\""; + +/// The standalone SVG document `fragment`, an inline `` element as +/// written in an HTML page, is signed as: `fragment` itself where it +/// already parses as XML to the tree the browser built from it, else that +/// tree serialized as XML. +/// +/// # Errors +/// +/// - [`Reason::MediaTooLarge`] where `fragment` is longer than `max_bytes`. +/// - [`Reason::SvgProvenanceNotEquivalent`] where `fragment` carries +/// provenance of its own and is not, as written, equivalent: it is never +/// rewritten. +/// - [`Reason::InlineSvgNotEquivalent`] where the browser's tree is not one +/// `` root, exceeds the node or depth bound, or has no XML +/// serialization that parses back to it as both HTML and XML. +pub(crate) fn signable_svg(fragment: &str, max_bytes: usize) -> Result, Reason> { + if fragment.len() > max_bytes { + return Err(Reason::MediaTooLarge); + } + let parsed = parse_html(fragment).ok_or(Reason::InlineSvgNotEquivalent)?; + let root = parsed.root().ok_or(Reason::InlineSvgNotEquivalent)?; + if let Some(document) = parse_xml(fragment) + && equal(&Browser, root, &Xml(PhantomData), document.root_element()) + { + return Ok(Cow::Borrowed(fragment)); + } + if carries_provenance(fragment, root) { + return Err(Reason::SvgProvenanceNotEquivalent); + } + let serialized = serialize(root, max_bytes).ok_or(Reason::InlineSvgNotEquivalent)?; + let equivalent = { + let as_html = parse_html(&serialized); + let as_xml = parse_xml(&serialized); + match (as_html.as_ref().and_then(Parsed::root), as_xml) { + (Some(as_html), Some(as_xml)) => { + equal(&Browser, root, &Browser, as_html) + && equal(&Browser, root, &Xml(PhantomData), as_xml.root_element()) + } + _ => false, + } + }; + if equivalent { + Ok(Cow::Owned(serialized)) + } else { + Err(Reason::InlineSvgNotEquivalent) + } +} + +/// Refuse all edits inside an original inline SVG that carries provenance. +/// A fragment whose structure cannot be inspected within the parser's limits +/// is also left intact, rather than assuming it has no provenance. +pub(crate) fn descendant_edit_refusal(fragment: &str) -> Option { + let Some(parsed) = parse_html(fragment) else { + return Some(Reason::InlineSvgNotEquivalent); + }; + let Some(root) = parsed.root() else { + return Some(Reason::InlineSvgNotEquivalent); + }; + carries_provenance(fragment, root).then_some(Reason::SvgExistingProvenance) +} + +/// Whether the inline SVG under `root` carries provenance of its own: the +/// manifest carrier the signer's writer finds, an element or attribute in +/// the `c2pa` prefix, a declaration of a C2PA namespace, or a provenance +/// reference such as XMP's `dcterms:provenance`. The HTML parser keeps such +/// prefixed names whole, as written. Ordinary text that mentions C2PA is +/// not provenance. +fn carries_provenance(fragment: &str, root: NodeRef<'_, Node>) -> bool { + if embedded::existing_carrier(fragment.as_bytes()).is_some() { + return true; + } + root.descendants().any(|node| { + let Node::Element(element) = node.value() else { + return false; + }; + names_provenance(&element.name.local) + || element.attrs.iter().any(|(name, value)| { + names_provenance(&name.local) + || (is_declaration(&name.ns, &name.local) && is_c2pa_namespace(value)) + }) + }) +} + +/// Whether a name the HTML parser kept whole, prefix included, is in the +/// `c2pa` prefix or names a provenance reference. +fn names_provenance(name: &str) -> bool { + let (prefix, local) = name.rsplit_once(':').unwrap_or(("", name)); + prefix.eq_ignore_ascii_case("c2pa") || local.eq_ignore_ascii_case("provenance") +} + +fn is_c2pa_namespace(uri: &str) -> bool { + let uri = uri.trim(); + ["http://c2pa.org/", "https://c2pa.org/"] + .iter() + .any(|start| { + uri.get(..start.len()) + .is_some_and(|head| head.eq_ignore_ascii_case(start)) + }) +} + +/// scraper's tree for one fragment, with its `` root. +struct Parsed { + html: scraper::Html, + root: ego_tree::NodeId, +} + +impl Parsed { + fn root(&self) -> Option> { + self.html.tree.get(self.root) + } +} + +/// `fragment` parsed as HTML in a `body` context with scripting on, as a +/// browser parses inline SVG, into scraper's tree, or `None` unless that +/// yields exactly one `` element at the top, with only whitespace text +/// beside it, within the node bound. +fn parse_html(fragment: &str) -> Option { + // Every element and comment the tokenizer starts begins at a `<`, so + // this bounds what the parse allocates before the exact count below. + if fragment.bytes().filter(|byte| *byte == b'<').count() > MAX_NODES { + return None; + } + let context = QualName::new( + None, + Namespace::from(HTML_NAMESPACE), + LocalName::from("body"), + ); + let html = parse_fragment( + HtmlTreeSink::new(scraper::Html::new_fragment()), + ParseOpts::default(), + context, + Vec::new(), + true, + ) + .one(fragment); + if html.tree.values().count() > MAX_NODES + PARSER_NODES { + return None; + } + let top = html + .tree + .root() + .children() + .find(|child| child.value().is_element())?; + let mut root = None; + for child in top.children() { + match child.value() { + Node::Element(element) + if root.is_none() + && &*element.name.ns == SVG_NAMESPACE + && &*element.name.local == "svg" => + { + root = Some(child.id()); + } + Node::Text(text) if text.chars().all(is_html_whitespace) => {} + _ => return None, + } + } + let root = root?; + Some(Parsed { html, root }) +} + +fn is_html_whitespace(character: char) -> bool { + matches!(character, '\t' | '\n' | '\u{c}' | '\r' | ' ') +} + +/// `text` parsed as XML with DTDs refused, so no entity is declared or +/// resolved, or `None` where it is not well-formed. +fn parse_xml(text: &str) -> Option> { + let options = roxmltree::ParsingOptions { + allow_dtd: false, + nodes_limit: u32::try_from(MAX_NODES + 1).unwrap_or(u32::MAX), + ..roxmltree::ParsingOptions::default() + }; + roxmltree::Document::parse_with_options(text, options).ok() +} + +/// What a compared node is. +enum Kind<'a> { + Element { namespace: &'a str, name: &'a str }, + Text(&'a str), + Comment(&'a str), + Other, +} + +/// A tree, as the comparison walks it. +trait View<'a> { + type Node: Copy; + type Children: Iterator; + + fn kind(&self, node: Self::Node) -> Kind<'a>; + /// The element's ordinary attributes as (namespace, local name, value), + /// sorted, namespace declarations excluded. + fn attributes(&self, node: Self::Node) -> Vec<(&'a str, &'a str, &'a str)>; + fn children(&self, node: Self::Node) -> Self::Children; +} + +/// Whether an attribute is a namespace declaration rather than an ordinary +/// attribute. The HTML parser puts `xmlns` and `xmlns:xlink` in the XMLNS +/// namespace and leaves any other `xmlns:` prefixed name unnamespaced. +fn is_declaration(namespace: &str, name: &str) -> bool { + namespace == XMLNS_NAMESPACE + || (namespace.is_empty() && (name == "xmlns" || name.starts_with("xmlns:"))) +} + +/// scraper's tree, whose node references carry the tree. +struct Browser; + +impl<'a> View<'a> for Browser { + type Node = NodeRef<'a, Node>; + type Children = Children<'a, Node>; + + fn kind(&self, node: Self::Node) -> Kind<'a> { + match node.value() { + Node::Element(element) => Kind::Element { + namespace: &element.name.ns, + name: &element.name.local, + }, + Node::Text(text) => Kind::Text(&text.text), + Node::Comment(comment) => Kind::Comment(&comment.comment), + _ => Kind::Other, + } + } + + fn attributes(&self, node: Self::Node) -> Vec<(&'a str, &'a str, &'a str)> { + let Node::Element(element) = node.value() else { + return Vec::new(); + }; + let mut ordinary: Vec<(&'a str, &'a str, &'a str)> = element + .attrs + .iter() + .map(|(name, value)| (&*name.ns, &*name.local, &**value)) + .filter(|(namespace, name, _)| !is_declaration(namespace, name)) + .collect(); + ordinary.sort_unstable(); + ordinary + } + + fn children(&self, node: Self::Node) -> Self::Children { + node.children() + } +} + +/// A roxmltree document, whose nodes carry their own references. +struct Xml<'a, 'input: 'a>(PhantomData>); + +impl<'a, 'input: 'a> View<'a> for Xml<'a, 'input> { + type Node = roxmltree::Node<'a, 'input>; + type Children = roxmltree::Children<'a, 'input>; + + fn kind(&self, node: Self::Node) -> Kind<'a> { + match node.node_type() { + roxmltree::NodeType::Element => { + let name = node.tag_name(); + Kind::Element { + namespace: name.namespace().unwrap_or_default(), + name: name.name(), + } + } + roxmltree::NodeType::Text => Kind::Text(node.text().unwrap_or_default()), + roxmltree::NodeType::Comment => Kind::Comment(node.text().unwrap_or_default()), + roxmltree::NodeType::Root | roxmltree::NodeType::PI => Kind::Other, + } + } + + fn attributes(&self, node: Self::Node) -> Vec<(&'a str, &'a str, &'a str)> { + // roxmltree keeps namespace declarations apart from attributes. + let mut ordinary: Vec<_> = node + .attributes() + .map(|attribute| { + ( + attribute.namespace().unwrap_or_default(), + attribute.name(), + attribute.value(), + ) + }) + .collect(); + ordinary.sort_unstable(); + ordinary + } + + fn children(&self, node: Self::Node) -> Self::Children { + node.children() + } +} + +/// Whether the trees under `a_root` and `b_root` are equal, within the +/// depth bound. The walk keeps one pair of child iterators per open element +/// and copies neither tree. +fn equal<'a, 'b, A: View<'a>, B: View<'b>>(a: &A, a_root: A::Node, b: &B, b_root: B::Node) -> bool { + if !same(a, a_root, b, b_root) { + return false; + } + let mut open = vec![(a.children(a_root), b.children(b_root))]; + while let Some((mut a_children, mut b_children)) = open.pop() { + match (a_children.next(), b_children.next()) { + (None, None) => {} + (Some(a_node), Some(b_node)) => { + if !same(a, a_node, b, b_node) { + return false; + } + let element = matches!(a.kind(a_node), Kind::Element { .. }); + open.push((a_children, b_children)); + if element { + if open.len() >= MAX_DEPTH { + return false; + } + open.push((a.children(a_node), b.children(b_node))); + } + } + _ => return false, + } + } + true +} + +fn same<'a, 'b, A: View<'a>, B: View<'b>>(a: &A, a_node: A::Node, b: &B, b_node: B::Node) -> bool { + match (a.kind(a_node), b.kind(b_node)) { + ( + Kind::Element { + namespace: a_namespace, + name: a_name, + }, + Kind::Element { + namespace: b_namespace, + name: b_name, + }, + ) => { + a_namespace == b_namespace + && a_name == b_name + && a.attributes(a_node) == b.attributes(b_node) + } + (Kind::Text(a_text), Kind::Text(b_text)) + | (Kind::Comment(a_text), Kind::Comment(b_text)) => a_text == b_text, + _ => false, + } +} + +/// The tree under `root` written as a standalone SVG document, or `None` +/// where XML cannot carry it: an element outside the SVG namespace, an +/// attribute outside no namespace, `XLink` or XML, a name XML cannot spell, a +/// character XML forbids, a comment XML cannot hold, any other node, or +/// more than `max_bytes`. +/// +/// The root declares the SVG namespace, and the `XLink` prefix where an +/// attribute uses it. Every element and ordinary attribute is the tree's, +/// with its value, which is the HTML parser's: the first of attributes that +/// differ only in case, and character references decoded. Values escape +/// `&`, `<`, `>`, `"`, tab, line feed and carriage return, and text escapes +/// `&`, `<`, `>` and carriage return, so XML's attribute-value and line-end +/// normalization leaves them as parsed. +fn serialize(root: NodeRef<'_, Node>, max_bytes: usize) -> Option { + let Node::Element(element) = root.value() else { + return None; + }; + let mut output = SvgOutput::new(max_bytes); + let mut uses_xlink = false; + write_start(&mut output, element, &mut uses_xlink)?; + // The root's declarations follow its name: the XLink one is known only + // once every attribute is written. + let declarations = 1 + element.name.local.len(); + output.insert_str(declarations, SVG_DECLARATION)?; + let mut open: Vec<(Children<'_, Node>, &str)> = Vec::new(); + if root.first_child().is_some() { + output.push('>')?; + open.push((root.children(), &*element.name.local)); + } else { + output.push_str("/>")?; + } + while let Some((mut children, name)) = open.pop() { + let Some(child) = children.next() else { + output.push_str("')?; + continue; + }; + open.push((children, name)); + match child.value() { + Node::Element(element) => { + write_start(&mut output, element, &mut uses_xlink)?; + if child.first_child().is_some() { + if open.len() >= MAX_DEPTH { + return None; + } + output.push('>')?; + open.push((child.children(), &*element.name.local)); + } else { + output.push_str("/>")?; + } + } + Node::Text(text) => write_text(&mut output, &text.text)?, + Node::Comment(comment) => write_comment(&mut output, &comment.comment)?, + _ => return None, + } + } + if uses_xlink { + output.insert_str(declarations + SVG_DECLARATION.len(), XLINK_DECLARATION)?; + } + Some(output.text) +} + +/// A serializer buffer whose length and requested allocation never exceed +/// the output limit. Grow geometrically, but request the exact bounded target +/// so String's implicit doubling cannot allocate beyond it. +struct SvgOutput { + text: String, + max_bytes: usize, +} + +impl SvgOutput { + fn new(max_bytes: usize) -> Self { + Self { + text: String::new(), + max_bytes, + } + } + + fn reserve(&mut self, additional: usize) -> Option<()> { + let needed = self.text.len().checked_add(additional)?; + if needed > self.max_bytes { + return None; + } + if needed > self.text.capacity() { + let target = self + .text + .capacity() + .checked_mul(2) + .unwrap_or(self.max_bytes) + .max(64.min(self.max_bytes)) + .max(needed) + .min(self.max_bytes); + self.text + .try_reserve_exact(target.checked_sub(self.text.len())?) + .ok()?; + } + Some(()) + } + + fn push_str(&mut self, text: &str) -> Option<()> { + self.reserve(text.len())?; + self.text.push_str(text); + Some(()) + } + + fn push(&mut self, character: char) -> Option<()> { + self.reserve(character.len_utf8())?; + self.text.push(character); + Some(()) + } + + fn insert_str(&mut self, at: usize, text: &str) -> Option<()> { + self.reserve(text.len())?; + self.text.insert_str(at, text); + Some(()) + } +} + +/// Writes ` Option<()> { + if &*element.name.ns != SVG_NAMESPACE || !is_xml_local_name(&element.name.local) { + return None; + } + output.push('<')?; + output.push_str(&element.name.local)?; + for (name, value) in element.attrs.iter() { + let namespace = &*name.ns; + let local = &*name.local; + if is_declaration(namespace, local) { + continue; + } + let prefix = match namespace { + "" => "", + XLINK_NAMESPACE => { + *uses_xlink = true; + "xlink:" + } + XML_NAMESPACE => "xml:", + _ => return None, + }; + if !is_xml_local_name(local) { + return None; + } + output.push(' ')?; + output.push_str(prefix)?; + output.push_str(local)?; + output.push_str("=\"")?; + for character in value.chars() { + match character { + '&' => output.push_str("&"), + '<' => output.push_str("<"), + '>' => output.push_str(">"), + '"' => output.push_str("""), + '\t' => output.push_str(" "), + '\n' => output.push_str(" "), + '\r' => output.push_str(" "), + character if is_xml_char(character) => output.push(character), + _ => return None, + }?; + } + output.push('"')?; + } + Some(()) +} + +fn write_text(output: &mut SvgOutput, text: &str) -> Option<()> { + for character in text.chars() { + match character { + '&' => output.push_str("&"), + '<' => output.push_str("<"), + '>' => output.push_str(">"), + '\r' => output.push_str(" "), + character if is_xml_char(character) => output.push(character), + _ => return None, + }?; + } + Some(()) +} + +/// Writes a comment XML can hold: no `--` inside, no `-` at the end. +fn write_comment(output: &mut SvgOutput, text: &str) -> Option<()> { + if text.contains("--") || text.ends_with('-') || !text.chars().all(is_xml_char) { + return None; + } + output.push_str("")?; + Some(()) +} + +/// A character XML 1.0 allows in a document. +fn is_xml_char(character: char) -> bool { + matches!( + character, + '\t' | '\n' + | '\r' + | '\u{20}'..='\u{d7ff}' + | '\u{e000}'..='\u{fffd}' + | '\u{10000}'..='\u{10ffff}' + ) +} + +/// A name XML can write without a prefix. Conservative: a name it refuses +/// is never written, and anything this admits that XML reads otherwise +/// fails the equivalence check. +fn is_xml_local_name(name: &str) -> bool { + let mut characters = name.chars(); + characters + .next() + .is_some_and(|first| first.is_alphabetic() || first == '_') + && characters + .all(|character| character.is_alphanumeric() || matches!(character, '-' | '_' | '.')) +} + +#[cfg(test)] +mod tests { + use super::*; + + const LIMIT: usize = 1024 * 1024; + + fn signed(fragment: &str) -> Result, Reason> { + signable_svg(fragment, LIMIT) + } + + /// The serialization `fragment` is signed as, checked to be a standalone + /// SVG document whose tree, read as XML and again as HTML, is the + /// browser's tree of `fragment`. + fn normalized(fragment: &str) -> String { + let Ok(Cow::Owned(serialized)) = signed(fragment) else { + panic!("should serialize {fragment}, got {:?}", signed(fragment)); + }; + let browser = parse_html(fragment).expect("should parse the original as HTML"); + let browser = browser.root().expect("should have an svg root"); + let reparsed = parse_html(&serialized).expect("should parse the serialization as HTML"); + let reparsed = reparsed.root().expect("should keep one svg root"); + let document = parse_xml(&serialized).expect("should be standalone XML"); + assert_eq!( + document.root_element().tag_name().namespace(), + Some(SVG_NAMESPACE), + "should declare the SVG namespace" + ); + assert!( + equal(&Browser, browser, &Browser, reparsed), + "should read back as the browser's tree: {serialized}" + ); + assert!( + equal( + &Browser, + browser, + &Xml(PhantomData), + document.root_element() + ), + "should read as XML to the browser's tree: {serialized}" + ); + serialized + } + + fn assert_unchanged(fragment: &str) { + assert!( + matches!(signed(fragment), Ok(Cow::Borrowed(kept)) if kept == fragment), + "should sign {fragment} as written, got {:?}", + signed(fragment) + ); + } + + /// The first SVG element named `name` in `serialized`, read as XML. + fn element<'a, 'input>( + document: &'a roxmltree::Document<'input>, + name: &str, + ) -> roxmltree::Node<'a, 'input> { + document + .descendants() + .find(|node| node.has_tag_name((SVG_NAMESPACE, name))) + .unwrap_or_else(|| panic!("should hold a {name} element")) + } + + #[test] + fn the_first_of_two_case_variant_attributes_is_the_one_signed() { + // The captured logo: the browser keeps `viewbox`'s value as + // `viewBox` and drops the later `viewBox`. + let serialized = normalized( + "\n \n", + ); + let document = parse_xml(&serialized).expect("should be XML"); + let root = document.root_element(); + assert_eq!(root.attribute("viewBox"), Some("0 0 300 300.251")); + assert_eq!(root.attribute("viewbox"), None); + assert_eq!(root.attribute("version"), Some("1.1")); + assert_eq!(element(&document, "path").attribute("d"), Some("M0 0h1")); + } + + #[test] + fn an_svg_without_its_namespace_is_signed_in_it() { + let serialized = normalized( + "", + ); + let document = parse_xml(&serialized).expect("should be XML"); + let root = document.root_element(); + assert_eq!(root.attribute("fill"), Some("currentColor")); + assert_eq!(root.attribute("aria-hidden"), Some("true")); + assert_eq!( + element(&document, "path").attribute("d"), + Some("M18.244 2.25h3.308") + ); + } + + #[test] + fn a_standalone_svg_is_signed_as_written_whatever_its_declarations() { + assert_unchanged( + "", + ); + assert_unchanged( + "", + ); + assert_unchanged( + "", + ); + assert_unchanged( + "&’&", + ); + } + + #[test] + fn an_xlink_attribute_is_signed_in_the_xlink_namespace() { + for fragment in [ + "", + // A wrong binding is a declaration the browser ignores. + "", + ] { + let serialized = normalized(fragment); + let document = parse_xml(&serialized).expect("should be XML"); + assert_eq!( + element(&document, "use").attribute((XLINK_NAMESPACE, "href")), + Some("#a"), + "for {fragment}" + ); + } + assert!( + !normalized("").contains("xmlns:xlink"), + "should declare XLink only where it is used" + ); + } + + #[test] + fn wrapped_attribute_whitespace_keeps_its_parsed_value() { + let serialized = normalized( + "", + ); + let document = parse_xml(&serialized).expect("should be XML"); + assert_eq!( + element(&document, "path").attribute("d"), + Some("M0 0\n L1 1\tZ") + ); + } + + #[test] + fn html_character_references_are_signed_as_their_characters() { + let serialized = normalized( + "A &<B’", + ); + let document = parse_xml(&serialized).expect("should be XML"); + assert_eq!(element(&document, "title").text(), Some("A\u{a0}&a�b"); + let document = parse_xml(&serialized).expect("should be XML"); + assert_eq!(element(&document, "text").text(), Some("a\u{fffd}b")); + } + + #[test] + fn cdata_is_signed_as_its_text() { + let serialized = normalized(""); + let document = parse_xml(&serialized).expect("should be XML"); + assert_eq!(element(&document, "style").text(), Some("a&e;"); + assert!(!serialized.contains("DOCTYPE"), "should carry no doctype"); + let document = parse_xml(&serialized).expect("should be XML"); + assert_eq!(element(&document, "text").text(), Some("&e;")); + } + + #[test] + fn markup_that_breaks_out_of_the_svg_is_refused() { + for fragment in [ + "

x

", + "
x
", + "
x
", + "x", + ] { + assert_eq!( + signed(fragment), + Err(Reason::InlineSvgNotEquivalent), + "should refuse {fragment}" + ); + } + } + + #[test] + fn what_xml_cannot_carry_is_refused() { + for fragment in [ + "", + "", + "", + "￾", + "", + "", + ] { + assert_eq!( + signed(fragment), + Err(Reason::InlineSvgNotEquivalent), + "should refuse {fragment}" + ); + } + } + + #[test] + fn provenance_the_svg_carries_is_never_rewritten() { + for fragment in [ + // The writer's carrier, without and with the SVG namespace. + "QUJD", + "QUJD", + // An unclosed carrier element. + "QUJD", + // A C2PA namespace declared for anything. + "", + // An XMP provenance reference, which need not name C2PA. + "", + "https://example.com/m", + ] { + assert_eq!( + signed(fragment), + Err(Reason::SvgProvenanceNotEquivalent), + "should leave {fragment} as written" + ); + } + } + + #[test] + fn a_caption_that_mentions_c2pa_is_not_provenance() { + let serialized = normalized( + "Signed with C2PAc2pa", + ); + let document = parse_xml(&serialized).expect("should be XML"); + assert_eq!(element(&document, "title").text(), Some("Signed with C2PA")); + } + + #[test] + fn serializer_checks_root_attributes_text_and_namespace_growth_before_writing() { + let fragment = ">&"; + let parsed = parse_html(fragment).expect("should parse"); + let root = parsed.root().expect("should have a root"); + let serialized = serialize(root, LIMIT).expect("should serialize"); + assert!(serialized.contains(SVG_DECLARATION)); + assert!(serialized.contains(XLINK_DECLARATION)); + for limit in 0..serialized.len() { + assert!( + serialize(root, limit).is_none(), + "must refuse limit {limit}" + ); + } + assert_eq!(serialize(root, serialized.len()), Some(serialized)); + + let fragment = format!("", "\"".repeat(1000)); + let parsed = parse_html(&fragment).expect("should parse a large root attribute"); + let Node::Element(element) = parsed.root().expect("should have a root").value() else { + panic!("should be an element"); + }; + let mut output = SvgOutput::new(127); + assert!(write_start(&mut output, element, &mut false).is_none()); + assert!(output.text.len() <= 127); + assert!(output.text.capacity() <= 127); + assert_eq!( + signable_svg(&fragment, 1100), + Err(Reason::InlineSvgNotEquivalent) + ); + + let fragment = format!("{}", ">".repeat(1000)); + assert_eq!( + signable_svg(&fragment, 1100), + Err(Reason::InlineSvgNotEquivalent) + ); + } + + #[test] + fn serializer_buffer_growth_is_bounded_amortized_and_not_the_media_limit() { + let mut output = SvgOutput::new(5_000_000); + assert_eq!(output.text.capacity(), 0); + output.push_str("", ">"), + ("\r", " "), + ] { + for limit in 0..escaped.len() { + let mut output = SvgOutput::new(limit); + assert!(write_text(&mut output, text).is_none()); + assert!(output.text.len() <= limit); + assert!(output.text.capacity() <= limit); + } + let mut output = SvgOutput::new(escaped.len()); + write_text(&mut output, text).expect("should fit the complete escape"); + assert_eq!(output.text, escaped); + } + for limit in 0..10 { + let mut output = SvgOutput::new(limit); + assert!(write_comment(&mut output, "art").is_none()); + assert!(output.text.len() <= limit); + assert!(output.text.capacity() <= limit); + } + let mut output = SvgOutput::new(10); + write_comment(&mut output, "art").expect("should fit the complete comment"); + assert_eq!(output.text, ""); + } + + #[test] + fn the_size_node_and_depth_bounds_refuse() { + let fragment = ""; + assert_eq!( + signable_svg(fragment, fragment.len() - 1), + Err(Reason::MediaTooLarge) + ); + let wide = format!( + "{}", + "".repeat(MAX_NODES) + ); + assert_eq!(signed(&wide), Err(Reason::InlineSvgNotEquivalent)); + let deep = format!( + "{}{}", + "".repeat(MAX_DEPTH), + "".repeat(MAX_DEPTH) + ); + assert_eq!(signed(&deep), Err(Reason::InlineSvgNotEquivalent)); + let nested = format!( + "{}{}", + "".repeat(MAX_DEPTH - 2), + "".repeat(MAX_DEPTH - 2) + ); + assert_unchanged(&nested); + } +} diff --git a/crates/provenance/encypher/src/tests.rs b/crates/provenance/encypher/src/tests.rs new file mode 100644 index 0000000000..5ad95b5baa --- /dev/null +++ b/crates/provenance/encypher/src/tests.rs @@ -0,0 +1,2243 @@ +use super::*; + +use std::sync::Arc; + +use base64::Engine as _; + +use edgezero_core::body::Body as EdgeBody; +use edgezero_core::key_value_store::KvPage; +use futures::executor::block_on; +use http::{Method, Request, header}; +use serde_json::json; +use trusted_server_core::auction::compile_auction_plan; +use trusted_server_core::auction::orchestrator::AuctionOrchestrator; +use trusted_server_core::cache_policy::EdgeCacheHeader; +use trusted_server_core::ec::EcContext; +use trusted_server_core::html_processor::test_support::page_settings; +use trusted_server_core::integrations::IntegrationRegistry; +use trusted_server_core::platform::test_support::{ + StubHttpClient, build_services_with_http_client, +}; +use trusted_server_core::platform::{ + KvError, PlatformHttpClient, PlatformKvStore, RuntimeServices, +}; +use trusted_server_core::publisher::{ + AppContext, AuctionDispatch, buffer_publisher_response_async, handle_publisher_request, + publisher_response_into_streaming_response, +}; + +const TOKEN: &str = "cwk_test_worker_token_value"; +const MARKER: &str = "\u{E0100}\u{E0101}\u{FE01}"; +const MANIFEST_URL: &str = "https://api.example.com/manifests/doc-1"; +const POLICY_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTICLE: &str = "Harbor Council Votes\ +

Harbor council votes on the ferry schedule

\ +

The harbor council met on Tuesday evening and approved a revised ferry schedule \ + for the coming season.

Residents asked for earlier departures on weekdays.

\ +
Footer
"; + +fn module_settings(table: &str, serve: bool) -> String { + let entry = if serve { + format!("[[serve]]\nmedia_type = \"text/html\"\nmiddleware = [\"{MODULE}\"]\n") + } else { + String::new() + }; + format!("[provenance]\nmodule = \"encypher\"\n\n[provenance.encypher]\n{table}\n{entry}") +} + +fn table() -> String { + format!( + "api_base_url = \"https://api.example.com\"\ndomain = \"publisher.example.com\"\n\ + worker_token = \"{TOKEN}\"\n" + ) +} + +fn placed() -> Settings { + page_settings(&module_settings(&table(), true)) +} + +fn config_value() -> serde_json::Value { + json!({ + "api_base_url": "https://api.example.com", + "domain": "publisher.example.com", + "worker_token": TOKEN, + }) +} + +fn parsed(value: &serde_json::Value) -> Result { + let config: EncypherConfig = + serde_json::from_value(value.clone()).map_err(|error| error.to_string())?; + config.validate().map_err(|error| error.to_string())?; + Ok(config) +} + +#[test] +fn module_constant_is_the_crate_folder() { + assert_eq!( + MODULE, + trusted_server_core::module_name!(), + "should be named by the folder this crate lives in" + ); +} + +#[test] +fn an_unselected_module_registers_nothing() { + let settings = page_settings(""); + + assert!(register(&settings).expect("should read").is_none()); + assert!(!validate(&settings).expect("should read")); +} + +#[test] +fn a_selected_and_placed_module_registers_its_middleware_and_media_signer() { + let settings = placed(); + + assert!(validate(&settings).expect("should validate")); + let registration = register(&settings) + .expect("should register") + .expect("a selected module with a worker token should register"); + let names: Vec<&str> = registration + .middleware + .iter() + .map(|middleware| middleware.middleware_id()) + .collect(); + assert_eq!( + names, + [MODULE, STYLESHEET_MIDDLEWARE], + "should supply the page and stylesheet middleware" + ); + assert_eq!( + registration + .media_signer + .as_ref() + .map(|signer| signer.signer_id()), + Some(MODULE), + "should register the media signer alongside the text path" + ); + let stylesheet = ®istration.middleware[1]; + assert!(stylesheet.handles_media_type("text/css")); + assert!( + stylesheet.handles_path("/assets/site.css"), + "the stylesheet path should not inherit the page path's asset exclusions" + ); +} + +#[test] +fn the_signer_declares_its_retained_media_cache() { + let mut settings = placed(); + settings.cache.declared.insert( + "assets".to_owned(), + trusted_server_core::settings::LoadingCacheConfig { + store_seconds: Some(120), + ..Default::default() + }, + ); + let builders = [builder()]; + assert_eq!(builders[0].cache_features(), &["assets"]); + trusted_server_core::integrations::loading_caches_from(&settings, &builders) + .expect("should accept the signer's existing retained-cache configuration"); +} + +#[test] +fn a_module_named_in_no_serve_entry_is_refused() { + let settings = page_settings(&module_settings(&table(), false)); + + let error = validate(&settings).expect_err("should refuse the document"); + + assert!( + format!("{error:?}").contains("no [[serve]] entry names"), + "should name the missing entry: {error:?}" + ); +} + +#[test] +fn a_blank_worker_token_registers_nothing() { + for token in ["", " \t"] { + let table = format!( + "api_base_url = \"https://api.example.com\"\ndomain = \"publisher.example.com\"\n\ + worker_token = \"{token}\"\n" + ); + let settings = page_settings(&module_settings(&table, true)); + + assert!( + register(&settings).expect("should read").is_none(), + "token {token:?} should not register" + ); + } +} + +#[test] +fn required_settings_and_bounds_are_enforced() { + for field in ["domain", "api_base_url", "worker_token"] { + let mut config = config_value(); + config + .as_object_mut() + .expect("should be an object") + .remove(field); + assert!( + parsed(&config).is_err(), + "config without {field} should fail" + ); + } + for (field, value) in [ + ("api_base_url", json!("http://api.example.com")), + ("api_base_url", json!("https://api.example.com/?q=1")), + ("domain", json!("Publisher.example.com")), + ("domain", json!("www.publisher.example.com")), + ("domain", json!("publisher.example.com/path")), + ("domain", json!("localhost")), + ("timeout_ms", json!(100)), + ("timeout_ms", json!(20_000)), + ("min_text_length", json!(10)), + ("negative_ttl_seconds", json!(10)), + ("policy_ttl_seconds", json!(301)), + ("article_selector", json!("a".repeat(92))), + ("max_media_bytes", json!(100)), + ("max_media_bytes", json!(8_000_001)), + ("max_media_sign_calls_per_document", json!(0)), + ("max_media_sign_calls_per_document", json!(129)), + ("max_large_media_bytes", json!(1)), + ( + "max_large_media_bytes", + json!(MAX_PREPARED_SOURCE_BYTES + 1), + ), + ("max_large_media_ms", json!(4_999)), + ("max_large_media_ms", json!(600_001)), + ("enabled", json!(true)), + ("path_prefixes", json!(["/news/"])), + ] { + let mut config = config_value(); + config[field] = value.clone(); + assert!( + parsed(&config).is_err(), + "{field} = {value} should be rejected" + ); + } + for value in [0, MIN_LARGE_MEDIA_BYTES, MAX_PREPARED_SOURCE_BYTES] { + let mut config = config_value(); + config["max_large_media_bytes"] = json!(value); + assert!( + parsed(&config).is_ok(), + "max_large_media_bytes = {value} should be accepted, 0 turning the lane off" + ); + } + for limit in [1, 64, 128] { + let mut config = config_value(); + config["max_media_sign_calls_per_document"] = json!(limit); + let accepted = parsed(&config).expect("bounded media-call limit"); + assert_eq!(accepted.max_media_sign_calls_per_document, limit); + } + for (max_hold_ms, timeout_ms, valid) in [ + (499, 250, false), + (500, 250, true), + (15_000, 3_000, true), + (15_001, 3_000, false), + (3_000, 3_000, false), + (3_001, 3_000, true), + ] { + let mut config = config_value(); + config["max_hold_ms"] = json!(max_hold_ms); + config["timeout_ms"] = json!(timeout_ms); + assert_eq!( + parsed(&config).is_ok(), + valid, + "max_hold_ms {max_hold_ms} with timeout_ms {timeout_ms}" + ); + } +} + +#[test] +fn article_selector_rejects_malformed_classes_at_settings_admission() { + for selector in [ + ".", + "..article", + ".123article", + ".article.body", + ".article body", + ".article>p", + ".article,main", + ".article#body", + ".article:hover", + ".article[data-body=yes]", + ".article\\:body", + ] { + let table = format!("{}article_selector = {}\n", table(), json!(selector)); + let settings = page_settings(&module_settings(&table, true)); + assert!( + validate(&settings).is_err(), + "malformed class selector {selector:?} should fail settings admission" + ); + assert!( + register(&settings).is_err(), + "malformed class selector {selector:?} should not register" + ); + } +} + +#[test] +fn article_selector_rejects_malformed_attributes_at_settings_admission() { + for selector in [ + "[", + "[itemprop]", + "[=articleBody]", + "[itemprop=]", + "[itemprop=articleBody", + "[itemprop=articleBody]p", + "[itemprop~=articleBody]", + "[itemprop=articleBody main]", + "[itemprop=articleBody=main]", + "[itemprop=[articleBody]]", + "[itemprop=\"articleBody']", + "[itemprop='articleBody' i]", + "[itemprop=articleBody][role=main]", + "[itemprop=articleBody],main", + "[itemprop='article\\Body']", + ] { + let table = format!("{}article_selector = {}\n", table(), json!(selector)); + let settings = page_settings(&module_settings(&table, true)); + assert!( + validate(&settings).is_err(), + "malformed attribute selector {selector:?} should fail settings admission" + ); + assert!( + register(&settings).is_err(), + "malformed attribute selector {selector:?} should not register" + ); + } +} + +#[test] +fn article_selector_rejects_malformed_tags_at_settings_admission() { + for selector in [ + " ", + " article", + "article ", + "123article", + "
", + "article.body", + "article p", + "article>p", + "article+p", + "article~p", + "article,main", + "article#body", + "article:hover", + "article[role=main]", + "#article", + "*", + ] { + let table = format!("{}article_selector = {}\n", table(), json!(selector)); + let settings = page_settings(&module_settings(&table, true)); + assert!( + validate(&settings).is_err(), + "malformed tag selector {selector:?} should fail settings admission" + ); + assert!( + register(&settings).is_err(), + "malformed tag selector {selector:?} should not register" + ); + } +} + +#[test] +fn article_selector_accepts_supported_syntax_and_selects_the_override() { + let selected = "Selected."; + let html = format!("
Fallback.
{selected}"); + for selector in [ + ".story-body", + "._story", + ".-story", + ".--story", + "[itemprop=articleBody]", + "[itemprop='articleBody']", + "[itemprop=\"articleBody\"]", + "[data-body='article body']", + "custom-story", + "CUSTOM-STORY", + ] { + let table = format!("{}article_selector = {}\n", table(), json!(selector)); + let settings = page_settings(&module_settings(&table, true)); + assert!( + validate(&settings).expect("should admit supported selector"), + "supported selector {selector:?} should select the module" + ); + assert!( + register(&settings) + .expect("should register supported selector") + .is_some(), + "supported selector {selector:?} should register" + ); + let boundary = boundary::find_article_boundary(&html, Some(selector)) + .expect("should find the configured override"); + assert_eq!( + boundary.selector, + format!("override:{selector}"), + "supported selector should take priority over the article fallback" + ); + assert_eq!(html.get(boundary.start..boundary.end), Some(selected)); + } +} + +#[test] +fn article_selector_empty_or_valid_but_absent_keeps_article_fallback() { + let selected = "

Fallback article.

"; + let html = format!("
Other region.
{selected}"); + for selector in ["", ".absent-story", "[data-body=absent]", "custom-story"] { + let table = format!("{}article_selector = {}\n", table(), json!(selector)); + let settings = page_settings(&module_settings(&table, true)); + assert!( + validate(&settings).expect("should admit empty or absent selector"), + "selector {selector:?} should select the module" + ); + let boundary = boundary::find_article_boundary(&html, Some(selector)) + .expect("should find the fallback article"); + assert_eq!(boundary.selector, "article"); + assert_eq!(html.get(boundary.start..boundary.end), Some(selected)); + } +} + +#[test] +fn excluded_paths_leave_the_middleware_out_of_the_chain() { + let middleware = Encypher { + signer: Arc::new(Signer::new( + parsed(&config_value()).expect("should parse"), + 5_000_000, + )), + }; + + assert!(middleware.handles_path("/news/harbor-vote")); + for path in [ + "/wp-admin/post", + "/feed/", + "/sitemap.xml", + "/app.js", + "/api/v1/x", + ] { + assert!(!middleware.handles_path(path), "{path} should be left out"); + } + assert_eq!(middleware.phases(), [MiddlewarePhase::Serve]); +} + +/// A runtime KV store held in memory. +#[derive(Default)] +struct MemoryKv(std::sync::Mutex>); + +#[async_trait::async_trait(?Send)] +impl PlatformKvStore for MemoryKv { + async fn get_bytes(&self, key: &str) -> Result, KvError> { + Ok(self.0.lock().expect("should lock").get(key).cloned()) + } + + async fn put_bytes(&self, key: &str, value: bytes::Bytes) -> Result<(), KvError> { + self.0 + .lock() + .expect("should lock") + .insert(key.to_owned(), value); + Ok(()) + } + + async fn put_bytes_with_ttl( + &self, + key: &str, + value: bytes::Bytes, + _ttl: std::time::Duration, + ) -> Result<(), KvError> { + self.put_bytes(key, value).await + } + + async fn delete(&self, key: &str) -> Result<(), KvError> { + self.0.lock().expect("should lock").remove(key); + Ok(()) + } + + async fn list_keys_page( + &self, + _prefix: &str, + _cursor: Option<&str>, + _limit: usize, + ) -> Result { + Ok(KvPage::default()) + } +} + +/// An origin answering [`ARTICLE`] with `cache_control`, then the policy and +/// sign answers of the API. +fn stub(cache_control: &str) -> Arc { + let stub = Arc::new(StubHttpClient::new()); + stub.push_response_with_headers( + 200, + ARTICLE.as_bytes().to_vec(), + vec![ + ("content-type", "text/html; charset=utf-8"), + ("cache-control", cache_control), + ], + ); + stub.push_response( + 200, + serde_json::to_vec(&json!({ "policy_digest": POLICY_DIGEST })).expect("should encode"), + ); + stub.push_response( + 200, + serde_json::to_vec(&json!({ + "success": true, + "policy_digest": POLICY_DIGEST, + "embedding_plan": { + "index_unit": "codepoint", + "operations": [{ "insert_after_index": 5, "marker": MARKER }], + }, + "manifest_url": MANIFEST_URL, + "verification_url": "https://api.example.com/verify/doc-1", + "document_id": "doc-1", + })) + .expect("should encode"), + ); + stub +} + +fn origin_stub(cache_control: &str) -> Arc { + let stub = Arc::new(StubHttpClient::new()); + stub.push_response_with_headers( + 200, + ARTICLE.as_bytes().to_vec(), + vec![ + ("content-type", "text/html; charset=utf-8"), + ("cache-control", cache_control), + ], + ); + stub +} + +fn page_request(method: Method) -> Request { + Request::builder() + .method(method) + .uri("https://publisher.example.com/news/harbor-vote?utm_source=feed") + .header(header::HOST, "publisher.example.com") + .body(EdgeBody::empty()) + .expect("should build the page request") +} + +/// The response a reader receives from the publisher path, through the +/// buffered or the streaming finalizer. +fn served_response( + settings: &Settings, + services: &RuntimeServices, + request: Request, + streaming: bool, +) -> http::Response { + try_served_response(settings, services, request, streaming).expect("should finish the page") +} + +fn try_served_response( + settings: &Settings, + services: &RuntimeServices, + request: Request, + streaming: bool, +) -> Result, error_stack::Report> { + let registry = IntegrationRegistry::with_registrations(settings, &[builder()]) + .expect("should build the registry"); + // As every adapter does, the request's services carry the registry's + // media signer. + let services = &services.clone().with_media_signer(registry.media_signer()); + let plan = compile_auction_plan(settings).expect("should compile the auction plan"); + let orchestrator = Arc::new(AuctionOrchestrator::from_plan(Arc::new(plan), None)); + let method = request.method().clone(); + block_on(async { + let mut ec_context = + EcContext::read_from_request_resolving_geo(settings, &request, services) + .await + .expect("should read the request's state"); + let publisher = handle_publisher_request( + AppContext { + settings, + integration_registry: ®istry, + }, + services, + None, + &mut ec_context, + AuctionDispatch { + orchestrator: &orchestrator, + slots: &[], + registry: None, + }, + request, + EdgeCacheHeader::SurrogateControl, + ) + .await?; + if streaming { + publisher_response_into_streaming_response( + publisher, + &method, + Arc::new(settings.clone()), + ®istry, + Arc::clone(&orchestrator), + services.clone(), + ) + .await + } else { + buffer_publisher_response_async( + publisher, + &method, + settings, + ®istry, + &orchestrator, + services, + ) + .await + } + }) +} + +/// The page body a reader receives from the publisher path. +fn served( + settings: &Settings, + services: &RuntimeServices, + request: Request, + streaming: bool, +) -> String { + let response = served_response(settings, services, request, streaming); + block_on(async { + let body = response + .into_body() + .into_bytes_bounded(16 * 1024 * 1024) + .await + .expect("should read the body"); + String::from_utf8(body.to_vec()).expect("should be UTF-8") + }) +} + +fn services_for(stub: &Arc) -> RuntimeServices { + services_for_kv(stub, Arc::new(MemoryKv::default())) +} + +fn services_for_kv(stub: &Arc, kv: Arc) -> RuntimeServices { + build_services_with_http_client(Arc::clone(stub) as Arc) + .with_kv_store(kv) +} + +#[test] +fn the_publisher_path_signs_an_article_through_both_finalizers() { + for streaming in [false, true] { + let stub = stub("public, max-age=60"); + let page = served( + &placed(), + &services_for(&stub), + page_request(Method::GET), + streaming, + ); + + assert!( + page.contains(MARKER), + "streaming={streaming}: should embed the marker: {page}" + ); + assert!( + page.contains("c2pa-manifest-url") && page.contains(MANIFEST_URL), + "streaming={streaming}: should write the manifest meta tag" + ); + assert_eq!( + stub.recorded_request_methods(), + ["GET", "GET", "POST"], + "streaming={streaming}: origin, policy, then sign" + ); + // The test request carries no TLS signal, so the scheme the publisher + // path detects is `http`. + let policy_uri = &stub.recorded_request_uris()[1]; + assert!( + policy_uri + .ends_with("page_url=http%3A%2F%2Fpublisher.example.com%2Fnews%2Fharbor-vote"), + "should send the page address without its query: {policy_uri}" + ); + } +} + +#[test] +fn an_article_on_a_publisher_addressed_by_path_is_signed_for_its_own_address() { + let stub = stub("public, max-age=60"); + let mut request = page_request(Method::GET); + request + .extensions_mut() + .insert(trusted_server_core::addressing::RemovedPathPrefix::new( + "/www.site.example".to_owned(), + )); + + let page = served(&placed(), &services_for(&stub), request, false); + + assert!(page.contains(MARKER), "should sign the article: {page}"); + let policy_uri = &stub.recorded_request_uris()[1]; + assert!( + policy_uri.ends_with( + "page_url=http%3A%2F%2Fpublisher.example.com%2Fwww.site.example%2Fnews%2Fharbor-vote" + ), + "should ask the policy for the address the reader asked for: {policy_uri}" + ); + let sign = String::from_utf8(stub.recorded_request_bodies()[2].clone()).expect("JSON body"); + assert!( + sign.contains( + "\"page_url\":\"http://publisher.example.com/www.site.example/news/harbor-vote\"" + ), + "should sign the article for the address the reader asked for: {sign}" + ); +} + +/// The unsigned bytes produced by the same public publisher pipeline before +/// its final writer applies the configured bound. Core middleware may rewrite +/// the origin body before Encypher sees it, so the origin fixture length is not +/// the final unsigned length. +fn unsigned_public_page() -> String { + let stub = origin_stub("public, max-age=60"); + let page = served( + &placed(), + &services_for(&stub), + page_request(Method::GET), + false, + ); + assert!( + page.len() > ARTICLE.len(), + "fixture must retain the HTML rewriter's output overhead" + ); + page +} + +#[test] +fn fresh_expansion_over_the_buffered_limit_returns_the_unsigned_page() { + let unsigned = unsigned_public_page(); + let mut settings = placed(); + settings.publisher.max_buffered_body_bytes = unsigned.len(); + let stub = stub("public, max-age=60"); + let response = served_response( + &settings, + &services_for(&stub), + page_request(Method::GET), + false, + ); + + assert_eq!(response.status(), 200); + let body = block_on(response.into_body().into_bytes_bounded(unsigned.len())) + .expect("the unsigned pipeline output should fit its exact buffered limit"); + assert_eq!(body.as_ref(), unsigned.as_bytes()); + assert_eq!(stub.recorded_request_methods(), ["GET", "GET", "POST"]); +} + +#[test] +fn cached_expansion_over_the_buffered_limit_returns_the_unsigned_page() { + let unsigned = unsigned_public_page(); + let kv = Arc::new(MemoryKv::default()); + let first = stub("public, max-age=60"); + let signed = served( + &placed(), + &services_for_kv(&first, Arc::clone(&kv)), + page_request(Method::GET), + false, + ); + assert!(signed.contains(MARKER), "should seed a valid plan"); + + let mut settings = placed(); + settings.publisher.max_buffered_body_bytes = unsigned.len(); + let cached = origin_stub("public, max-age=60"); + let response = served_response( + &settings, + &services_for_kv(&cached, kv), + page_request(Method::GET), + false, + ); + + assert_eq!(response.status(), 200); + let body = block_on(response.into_body().into_bytes_bounded(unsigned.len())) + .expect("the unsigned pipeline output should fit its exact buffered limit"); + assert_eq!(body.as_ref(), unsigned.as_bytes()); + assert_eq!( + cached.recorded_request_methods(), + ["GET"], + "the cached policy and plan should make no Encypher API request" + ); +} + +#[test] +fn a_page_the_origin_marked_private_is_never_sent() { + let stub = stub("private, max-age=0"); + + let page = served( + &placed(), + &services_for(&stub), + page_request(Method::GET), + false, + ); + + assert!( + !page.contains(MARKER), + "should not sign a reader's own page" + ); + assert_eq!( + stub.recorded_request_methods(), + ["GET"], + "should call the origin and nothing else" + ); +} + +/// The pixels of a small image an origin embeds or serves. +const SOURCE_IMAGE: &[u8] = b"\x89PNG\r\n\x1a\n-source-pixels"; +/// The same image with an embedded C2PA manifest, as the media endpoint +/// answers it. +const SIGNED_IMAGE: &[u8] = b"\x89PNG\r\n\x1a\n-source-pixels-caBX-jumb-c2pa-manifest"; + +fn encoded(bytes: &[u8]) -> String { + base64::engine::general_purpose::STANDARD.encode(bytes) +} + +/// The headers of an embedded-manifest answer of the media endpoint for +/// `body` of `content_type`. +fn media_answer_as(body: &[u8], content_type: &str) -> Vec<(String, String)> { + [ + ("content-type", content_type.to_owned()), + ("x-encypher-policy-digest", POLICY_DIGEST.to_owned()), + ("x-encypher-signed-sha256", media_api::sha256_hex(body)), + ("x-encypher-manifest-instance-id", "xmp:iid:0001".to_owned()), + ("x-encypher-manifest-disposition", "embedded".to_owned()), + ("x-encypher-manifest-link-required", "false".to_owned()), + ( + "x-encypher-verification-url", + "https://verify.example.com/doc-2".to_owned(), + ), + ("x-encypher-document-id", "doc-2".to_owned()), + ] + .into_iter() + .map(|(name, value)| (name.to_owned(), value)) + .collect() +} + +/// [`ARTICLE`] with a lazily loaded embedded image inside the article. +fn article_with_image() -> String { + ARTICLE.replace( + "
", + &format!( + "\"Harbor\"
", + encoded(SOURCE_IMAGE) + ), + ) +} + +/// An origin answering `body` as `content_type`, publicly cacheable. +fn origin_answer(stub: &StubHttpClient, body: &[u8], content_type: &str) { + stub.push_response_with_headers( + 200, + body.to_vec(), + vec![ + ("content-type", content_type), + ("cache-control", "public, max-age=60"), + ], + ); +} + +/// The policy answer the media lane asks for live, before it signs or reuses +/// embedded media. +fn live_policy(stub: &StubHttpClient) { + stub.push_response( + 200, + serde_json::to_vec(&json!({ "policy_digest": POLICY_DIGEST })).expect("should encode"), + ); +} + +#[test] +fn a_revoked_credential_stops_embedded_media_reuse_while_the_cached_policy_lives() { + let kv = Arc::new(MemoryKv::default()); + let first = Arc::new(StubHttpClient::new()); + origin_answer( + &first, + article_with_image().as_bytes(), + "text/html; charset=utf-8", + ); + text_answers(&first); + live_policy(&first); + first.push_response_with_headers(200, SIGNED_IMAGE.to_vec(), media_answer(SIGNED_IMAGE)); + let warmed = served( + &placed(), + &services_for_kv(&first, Arc::clone(&kv)), + page_request(Method::GET), + false, + ); + assert!( + warmed.contains(&encoded(SIGNED_IMAGE)), + "should warm the policy and media caches" + ); + + // The cached policy and the cached signed image are both still fresh; + // the service now refuses the credential. + let revoked = Arc::new(StubHttpClient::new()); + origin_answer( + &revoked, + article_with_image().as_bytes(), + "text/html; charset=utf-8", + ); + revoked.push_response(403, b"{}".to_vec()); + let page = served( + &placed(), + &services_for_kv(&revoked, kv), + page_request(Method::GET), + false, + ); + + assert!( + !page.contains(&encoded(SIGNED_IMAGE)), + "should never reuse a signed image under a revoked credential" + ); + assert!( + page.contains(&encoded(SOURCE_IMAGE)), + "should deliver the image as the origin wrote it" + ); + assert!( + !revoked + .recorded_request_methods() + .iter() + .any(|method| method == "POST"), + "should sign nothing" + ); +} + +/// The policy and article-sign answers [`stub`] queues after the origin. +fn text_answers(stub: &StubHttpClient) { + stub.push_response( + 200, + serde_json::to_vec(&json!({ "policy_digest": POLICY_DIGEST })).expect("should encode"), + ); + stub.push_response( + 200, + serde_json::to_vec(&json!({ + "success": true, + "policy_digest": POLICY_DIGEST, + "embedding_plan": { + "index_unit": "codepoint", + "operations": [{ "insert_after_index": 5, "marker": MARKER }], + }, + "manifest_url": MANIFEST_URL, + })) + .expect("should encode"), + ); +} + +#[test] +fn the_publisher_path_signs_the_article_then_its_embedded_image() { + for streaming in [false, true] { + let stub = Arc::new(StubHttpClient::new()); + origin_answer( + &stub, + article_with_image().as_bytes(), + "text/html; charset=utf-8", + ); + text_answers(&stub); + live_policy(&stub); + stub.push_response_with_headers(200, SIGNED_IMAGE.to_vec(), media_answer(SIGNED_IMAGE)); + + let page = served( + &placed(), + &services_for(&stub), + page_request(Method::GET), + streaming, + ); + + assert!( + page.contains(MARKER), + "streaming={streaming}: should sign the text" + ); + assert!( + page.contains(&format!( + "src=\"data:image/png;base64,{}\" alt=\"Harbor\"", + encoded(SIGNED_IMAGE) + )), + "streaming={streaming}: should deliver the signed image bytes in place: {page}" + ); + assert!( + !page.contains(&encoded(SOURCE_IMAGE)), + "streaming={streaming}: should leave no unsigned copy" + ); + assert_eq!( + stub.recorded_request_methods(), + ["GET", "GET", "POST", "GET", "POST"], + "streaming={streaming}: origin, policy, text sign, live policy, media sign" + ); + assert!( + stub.recorded_request_uris()[4].ends_with("/api/v1/public/cdn/media/sign"), + "streaming={streaming}: should call the media endpoint" + ); + let media_body = &stub.recorded_request_bodies()[4]; + assert!( + media_body + .windows(SOURCE_IMAGE.len()) + .any(|window| window == SOURCE_IMAGE), + "streaming={streaming}: should send the decoded source bytes" + ); + } +} + +#[test] +fn a_refused_embedded_image_stays_as_written_and_the_text_is_still_signed() { + let stub = Arc::new(StubHttpClient::new()); + origin_answer( + &stub, + article_with_image().as_bytes(), + "text/html; charset=utf-8", + ); + text_answers(&stub); + live_policy(&stub); + stub.push_response( + 422, + serde_json::to_vec(&json!({ + "success": false, + "error": {"code": "UNSUPPORTED_MEDIA_TYPE", "message": "x"}, + })) + .expect("should encode"), + ); + + let page = served( + &placed(), + &services_for(&stub), + page_request(Method::GET), + false, + ); + + assert!(page.contains(MARKER), "should still sign the text"); + assert!( + page.contains(&encoded(SOURCE_IMAGE)), + "should leave the unsigned image as the origin wrote it" + ); + assert!( + !page.contains(&encoded(SIGNED_IMAGE)), + "should never present unsigned bytes as signed" + ); +} + +/// The module's settings with a stylesheet entry, scoped by `path` when one +/// is given. +fn stylesheet_settings(path: Option<&str>) -> String { + let path = path.map_or(String::new(), |path| format!("path = \"{path}\"\n")); + format!( + "{}\n[[serve]]\nmedia_type = \"text/css\"\n{path}middleware = [\"{STYLESHEET_MIDDLEWARE}\"]\n", + module_settings(&table(), true) + ) +} + +#[test] +fn a_stylesheet_entry_for_every_path_is_refused() { + for path in [None, Some("/")] { + let settings = page_settings(&stylesheet_settings(path)); + let error = validate(&settings).expect_err("should refuse an unscoped entry"); + assert!( + format!("{error:?}").contains("integrity"), + "{path:?}: should explain the integrity risk: {error:?}" + ); + } + let scoped = page_settings(&stylesheet_settings(Some("/assets/"))); + assert!(validate(&scoped).expect("should accept a scoped entry")); +} + +#[test] +fn the_publisher_path_signs_media_a_stylesheet_embeds() { + let settings = page_settings(&stylesheet_settings(Some("/assets/"))); + let stylesheet = format!( + ".hero{{background-image:url(\"data:image/png;base64,{}\")}}", + encoded(SOURCE_IMAGE) + ); + let stub = Arc::new(StubHttpClient::new()); + origin_answer(&stub, stylesheet.as_bytes(), "text/css; charset=utf-8"); + stub.push_response( + 200, + serde_json::to_vec(&json!({ "policy_digest": POLICY_DIGEST })).expect("should encode"), + ); + live_policy(&stub); + stub.push_response_with_headers(200, SIGNED_IMAGE.to_vec(), media_answer(SIGNED_IMAGE)); + let request = Request::builder() + .method(Method::GET) + .uri("https://publisher.example.com/assets/site.css") + .header(header::HOST, "publisher.example.com") + .body(EdgeBody::empty()) + .expect("should build the stylesheet request"); + + let served_stylesheet = served(&settings, &services_for(&stub), request, false); + + assert_eq!( + served_stylesheet, + format!( + ".hero{{background-image:url(\"data:image/png;base64,{}\")}}", + encoded(SIGNED_IMAGE) + ), + "should replace the embedded image with its signed bytes and nothing else" + ); +} + +/// The headers of an embedded-manifest PNG answer for `body`. +fn media_answer(body: &[u8]) -> Vec<(String, String)> { + media_answer_as(body, "image/png") +} + +fn media_request(path: &str) -> Request { + Request::builder() + .method(Method::GET) + .uri(format!("https://publisher.example.com{path}")) + .header(header::HOST, "publisher.example.com") + .header(header::ACCEPT, "*/*") + .body(EdgeBody::empty()) + .expect("should build the media request") +} + +fn body_of(response: http::Response) -> bytes::Bytes { + block_on(response.into_body().into_bytes_bounded(16 * 1024 * 1024)) + .expect("should read the body") +} + +#[test] +fn the_publisher_path_delivers_the_signed_bytes_of_images_audio_and_video_on_any_path() { + for (path, content_type) in [ + ("/images/harbor.png", "image/png"), + ("/media/4711", "image/png"), + ("/video/ferry.mp4", "video/mp4"), + ("/audio/council.mp3", "audio/mpeg"), + ] { + let stub = Arc::new(StubHttpClient::new()); + origin_answer(&stub, SOURCE_IMAGE, content_type); + stub.push_response( + 200, + serde_json::to_vec(&json!({ "policy_digest": POLICY_DIGEST })).expect("should encode"), + ); + stub.push_response_with_headers( + 200, + SIGNED_IMAGE.to_vec(), + media_answer_as(SIGNED_IMAGE, content_type), + ); + + let response = served_response(&placed(), &services_for(&stub), media_request(path), false); + + assert_eq!(response.status(), 200, "{path}: should answer"); + let headers = response.headers().clone(); + assert_eq!( + headers + .get(header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()), + Some(content_type), + "{path}: should keep the media type" + ); + assert_eq!( + headers + .get("x-encypher-document-id") + .and_then(|value| value.to_str().ok()), + Some("doc-2"), + "{path}: should name the signed document" + ); + assert_eq!( + headers + .get(header::ETAG) + .and_then(|value| value.to_str().ok()), + Some(format!("\"sig-{}\"", media_api::sha256_hex(SIGNED_IMAGE)).as_str()), + "{path}: should validate the signed bytes, not the origin's" + ); + assert_eq!( + body_of(response).as_ref(), + SIGNED_IMAGE, + "{path}: should deliver the exact signed bytes" + ); + assert_eq!( + stub.recorded_request_methods(), + ["GET", "GET", "POST"], + "{path}: origin, policy, media sign" + ); + let metadata = &stub.recorded_request_bodies()[2]; + assert!( + metadata + .windows(SOURCE_IMAGE.len()) + .any(|window| window == SOURCE_IMAGE), + "{path}: should send the captured origin bytes" + ); + assert!( + metadata + .windows(b"\"origin_host\":\"".len()) + .any(|window| window == b"\"origin_host\":\""), + "{path}: should name the host core fetched from" + ); + } +} + +#[test] +fn a_failed_media_signature_delivers_the_origin_bytes_unmarked() { + let stub = Arc::new(StubHttpClient::new()); + origin_answer(&stub, SOURCE_IMAGE, "image/png"); + stub.push_response( + 200, + serde_json::to_vec(&json!({ "policy_digest": POLICY_DIGEST })).expect("should encode"), + ); + // An answer whose bytes are not the ones it names is never delivered. + stub.push_response_with_headers( + 200, + b"\x89PNG\r\n\x1a\nswapped-c2pa".to_vec(), + media_answer(SIGNED_IMAGE), + ); + + let response = served_response( + &placed(), + &services_for(&stub), + media_request("/images/harbor.png"), + false, + ); + + assert!( + response.headers().get("x-encypher-document-id").is_none(), + "should claim no signature" + ); + assert_eq!( + body_of(response).as_ref(), + SOURCE_IMAGE, + "should deliver the origin's bytes as they were" + ); +} + +#[test] +fn a_media_segment_passes_through_as_the_origin_sent_it() { + // A C2PA verifiable-segment-info segment is bound by its stream's init + // manifest. Signing it alone would fail, and the unsigned fallback would + // make it private and drop its validators. + const SEGMENT: &[u8] = b"\0\0\0\x18stypmsdh\0\0\0\0msdhmsixmoof-mdat-emsg"; + let stub = Arc::new(StubHttpClient::new()); + stub.push_response_with_headers( + 200, + SEGMENT.to_vec(), + vec![ + ("content-type", "video/iso.segment"), + ("cache-control", "public, max-age=60"), + ("etag", "\"segment-7\""), + ], + ); + + let response = served_response( + &placed(), + &services_for(&stub), + media_request("/live/ferry/segment-7.m4s"), + false, + ); + + assert_eq!(response.status(), 200, "should answer"); + let headers = response.headers().clone(); + assert_eq!( + headers + .get(header::ETAG) + .and_then(|value| value.to_str().ok()), + Some("\"segment-7\""), + "should keep the origin's validator" + ); + assert!( + headers + .get(header::CACHE_CONTROL) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| !value.contains("private")), + "should not make the segment private" + ); + assert!( + headers.get("x-encypher-document-id").is_none(), + "should claim no signature" + ); + assert_eq!( + body_of(response).as_ref(), + SEGMENT, + "should deliver the segment byte for byte" + ); + assert_eq!( + stub.recorded_request_methods(), + ["GET"], + "should ask neither the policy nor the signing endpoint" + ); +} + +/// One ISO BMFF box of `kind` around `body`. +fn bmff_box(kind: &[u8; 4], body: &[u8]) -> Vec { + let size = u32::try_from(body.len() + 8).expect("should fit a 32-bit size"); + [&size.to_be_bytes()[..], kind, body].concat() +} + +#[test] +fn fragmented_stream_parts_typed_as_mp4_pass_through_as_the_origin_sent_them() { + let init = [ + bmff_box(b"ftyp", b"cmfc\0\0\0\0iso6cmfc"), + bmff_box( + b"moov", + &[ + bmff_box(b"mvhd", &[0; 100]), + bmff_box(b"mvex", &bmff_box(b"trex", &[0; 24])), + bmff_box(b"pssh", &[0; 32]), + ] + .concat(), + ), + ] + .concat(); + let segment = [ + bmff_box(b"styp", b"msdh\0\0\0\0msdhmsix"), + bmff_box(b"emsg", b"\0\0\0\0urn:c2pa:verifiable-segment-info\0fseg\0"), + bmff_box(b"sidx", &[0; 32]), + bmff_box(b"moof", &bmff_box(b"mfhd", &[0; 8])), + bmff_box(b"mdat", b"coded-samples"), + ] + .concat(); + for (path, part) in [ + ("/live/ferry/init.mp4", init), + ("/live/ferry/7.m4s", segment), + ] { + // No `no-transform`: the bytes alone keep the part from being signed. + let stub = Arc::new(StubHttpClient::new()); + stub.push_response_with_headers( + 200, + part.clone(), + vec![ + ("content-type", "video/mp4"), + ("cache-control", "public, max-age=60"), + ("etag", "\"part-7\""), + ], + ); + + let response = served_response(&placed(), &services_for(&stub), media_request(path), false); + + assert_eq!(response.status(), 200, "{path}: should answer"); + let headers = response.headers().clone(); + assert_eq!( + headers + .get(header::ETAG) + .and_then(|value| value.to_str().ok()), + Some("\"part-7\""), + "{path}: should keep the origin's validator" + ); + assert!( + headers + .get(header::CACHE_CONTROL) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| !value.contains("private")), + "{path}: should not make the part private" + ); + assert!( + headers.get(header::LINK).is_none() && headers.get("x-encypher-document-id").is_none(), + "{path}: should add no manifest" + ); + assert_eq!( + body_of(response).as_ref(), + part.as_slice(), + "{path}: should deliver the part byte for byte" + ); + assert_eq!( + stub.recorded_request_methods(), + ["GET"], + "{path}: should ask neither the policy nor the signing endpoint" + ); + } +} + +#[test] +fn a_progressive_mp4_is_still_handed_to_the_signing_endpoint() { + let progressive = [ + bmff_box(b"ftyp", b"isom\0\0\0\0isomavc1"), + bmff_box(b"moov", &bmff_box(b"trak", &[0; 16])), + bmff_box(b"mdat", b"coded-samples"), + ] + .concat(); + let stub = Arc::new(StubHttpClient::new()); + origin_answer(&stub, &progressive, "video/mp4"); + live_policy(&stub); + stub.push_response_with_headers( + 200, + SIGNED_IMAGE.to_vec(), + media_answer_as(SIGNED_IMAGE, "video/mp4"), + ); + + let _response = served_response( + &placed(), + &services_for(&stub), + media_request("/video/ferry.mp4"), + false, + ); + + assert_eq!( + stub.recorded_request_methods(), + ["GET", "GET", "POST"], + "origin, policy, media sign" + ); + let sent = &stub.recorded_request_bodies()[2]; + assert!( + sent.windows(progressive.len()) + .any(|window| window == progressive.as_slice()), + "should send the progressive file's exact bytes" + ); +} + +/// Signed source bytes deliberately contain targets of core's attribute and +/// text URL replacement before the provenance carrier/reference appears. +fn protected_svg(reference: bool) -> String { + let provenance = if reference { + "" + } else { + "QUJD" + }; + format!( + "\ + \ + https://origin.example.com/\ + \ + \ + {}{provenance}", + encoded(SOURCE_IMAGE), + "origin.example.com ".repeat(1024), + ) +} + +fn article_with_protected_svg(svg: &str) -> String { + ARTICLE.replace( + "
", + &format!( + "{svg}\ + https://origin.example.com/outside
" + ), + ) +} + +fn assert_source_and_surroundings(page: &str, svg: &str) { + assert!( + page.contains(svg), + "must retain the complete original SVG byte slice" + ); + assert!( + page.contains("http://publisher.example.com/outside"), + "core's ordinary outside URL edits must still run: {page}" + ); + assert!(page.contains(MARKER), "article signing must still run"); + assert!( + !page.contains("` and `` are whole comments, and `--!>` also ends one. +pub(crate) fn comment_end(bytes: &[u8], start: usize) -> Option { + let body = start + 4; + if starts_with_at(bytes, body, b">") { + return Some(body + 1); + } + if starts_with_at(bytes, body, b"->") { + return Some(body + 2); + } + let dash = find_bytes(bytes, b"-->", body).map(|end| end + 3); + let bang = find_bytes(bytes, b"--!>", body).map(|end| end + 4); + match (dash, bang) { + (Some(dash), Some(bang)) => Some(dash.min(bang)), + (end, None) | (None, end) => end, + } +} + +/// Tokenizer states between a tag's name and its closing `>`. +#[derive(Clone, Copy)] +enum TagState { + Name, + BeforeAttributeName, + AttributeName, + BeforeValue, + Quoted(u8), + Unquoted, +} + +/// Returns the offset of the `>` ending the tag whose name starts at `start`. +/// As in the HTML tokenizer, a quote delimits only a value it begins. +pub(crate) fn tag_end(bytes: &[u8], start: usize) -> Option { + let mut state = TagState::Name; + for (offset, byte) in bytes.get(start..)?.iter().copied().enumerate() { + let space = byte.is_ascii_whitespace(); + state = match state { + TagState::Quoted(quote) => { + if byte == quote { + TagState::BeforeAttributeName + } else { + state + } + } + _ if byte == b'>' => return Some(start + offset), + TagState::Name if space || byte == b'/' => TagState::BeforeAttributeName, + TagState::BeforeAttributeName if !space && byte != b'/' => TagState::AttributeName, + TagState::AttributeName if byte == b'=' => TagState::BeforeValue, + TagState::AttributeName if byte == b'/' => TagState::BeforeAttributeName, + TagState::BeforeValue if byte == b'"' || byte == b'\'' => TagState::Quoted(byte), + TagState::BeforeValue if !space => TagState::Unquoted, + TagState::Unquoted if space => TagState::BeforeAttributeName, + _ => state, + }; + } + None +} + +fn match_literal_suffix(units: &I, expected: &str) -> Option<(I, usize)> +where + I: Clone + Iterator, +{ + let mut trial = units.clone(); + let mut end = None; + for expected_char in expected.chars() { + let item = trial.next()?; + if item.ch != expected_char { + return None; + } + end = Some(item.end); + } + Some((trial, end?)) +} + +fn match_numeric_suffix(units: &I, hexadecimal: bool) -> Option<(I, char, usize)> +where + I: Clone + Iterator, +{ + let mut trial = units.clone(); + consume_expected(&mut trial, '#')?; + if hexadecimal { + consume_expected(&mut trial, 'x')?; + } + + let radix = if hexadecimal { 16 } else { 10 }; + let mut value = 0_u32; + let mut has_digits = false; + loop { + let item = trial.next()?; + let digit = if (hexadecimal && item.ch.is_ascii_hexdigit()) + || (!hexadecimal && item.ch.is_ascii_digit()) + { + item.ch.to_digit(radix) + } else { + None + }; + if let Some(digit) = digit { + has_digits = true; + value = value.checked_mul(radix)?.checked_add(digit)?; + } else if has_digits && item.ch == ';' { + return Some((trial, char::from_u32(value)?, item.end)); + } else { + return None; + } + } +} + +fn consume_expected(units: &mut impl Iterator, expected: char) -> Option<()> { + (units.next()?.ch == expected).then_some(()) +} + +pub(crate) fn is_js_whitespace(ch: char) -> bool { + matches!( + ch, + '\u{0009}'..='\u{000d}' + | '\u{0020}' + | '\u{00a0}' + | '\u{1680}' + | '\u{2000}'..='\u{200a}' + | '\u{2028}' + | '\u{2029}' + | '\u{202f}' + | '\u{205f}' + | '\u{3000}' + | '\u{feff}' + ) +} + +pub(crate) fn find_byte(bytes: &[u8], needle: u8, start: usize) -> Option { + let relative = bytes + .get(start..)? + .iter() + .position(|byte| *byte == needle)?; + Some(start.saturating_add(relative)) +} + +pub(crate) fn find_bytes(bytes: &[u8], needle: &[u8], start: usize) -> Option { + if needle.is_empty() { + return Some(start.min(bytes.len())); + } + let haystack = bytes.get(start..)?; + let relative = haystack + .windows(needle.len()) + .position(|window| window == needle)?; + Some(start.saturating_add(relative)) +} + +pub(crate) fn starts_with_at(bytes: &[u8], start: usize, needle: &[u8]) -> bool { + bytes + .get(start..) + .is_some_and(|tail| tail.starts_with(needle)) +} + +fn structural_tag<'a>(start: usize, token: &'a str, mode: ScanMode) -> Option> { + let content = token.strip_prefix('<')?; + let (kind, content) = if let Some(content) = content.strip_prefix('/') { + (TagKind::Closing, content) + } else { + let content = match mode { + ScanMode::Canonical => content.trim_start_matches(is_js_whitespace), + ScanMode::Structural => content, + }; + (TagKind::Opening, content) + }; + let name = take_tag_name(content)?; + Some(StructuralTag { + start, + end: start.checked_add(token.len())?, + name, + kind, + token, + }) +} + +pub(crate) fn is_structural_element_start(bytes: &[u8], start: usize) -> bool { + match bytes.get(start + 1).copied() { + Some(byte) if byte.is_ascii_alphabetic() => true, + Some(b'/') => bytes.get(start + 2).is_some_and(u8::is_ascii_alphabetic), + _ => false, + } +} + +pub(crate) fn raw_close_name_end(bytes: &[u8], start: usize, expected: &str) -> Option { + if !starts_with_at(bytes, start, b"')) + .then_some(name_end) +} + +pub(crate) fn take_tag_name(content: &str) -> Option<&str> { + let end = content + .char_indices() + .find(|(_, ch)| ch.is_ascii_whitespace() || matches!(*ch, '/' | '>')) + .map_or(content.len(), |(index, _)| index); + (end > 0).then(|| content.get(..end)).flatten() +} + +fn is_canonical_ignored(name: &str) -> bool { + TEXT_IGNORED.iter().any(|expected| { + let Some(prefix) = name.get(..expected.len()) else { + return false; + }; + prefix.eq_ignore_ascii_case(expected) + && name + .as_bytes() + .get(expected.len()) + .is_none_or(|byte| !byte.is_ascii_alphanumeric() && *byte != b'_') + }) +} + +fn contains_tag(tags: &[&str], name: &str) -> bool { + tags.iter() + .any(|candidate| name.eq_ignore_ascii_case(candidate)) +} + +/// Matches edge-core's `/\/\s*>$/` test on a complete tag token. +fn is_self_closing(token: &str) -> bool { + token + .strip_suffix('>') + .is_some_and(|body| body.trim_end_matches(is_js_whitespace).ends_with('/')) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn canonical_text(html: &str) -> String { + visible_text(html, usize::MAX).0 + } + + fn title(html: &str) -> Option { + let metadata = document_metadata(html); + title_from_range(html, metadata.title_range) + } + + #[test] + fn quoted_markup_and_unmatched_script_tags_stay_inert() { + for html in [ + r#"
Visible
"#, + r#"
Visible
"#, + ] { + assert_eq!(document_metadata(html).head_close, html.rfind("")); + assert_eq!(canonical_text(html), "Visible"); + } + } + + #[test] + fn browser_end_tag_and_comment_forms_close_where_browsers_do() { + // `` ends the script and `` is a whole comment, so each + // later `` sits inside an attribute value. + for prefix in [ + r#""#, + r#""#, + ] { + let html = format!("{prefix}
Visible
"); + assert_eq!(document_metadata(&html).head_close, Some(prefix.len())); + } + assert_eq!( + canonical_text("abce"#, + r#""#, + r#""#, + r#""#, + r#""#, + ); + let base = super::effective_document_base(source.as_bytes(), &document).expect("base"); + assert_eq!( + base.as_str(), + "https://cdn.example.net/npm/lib/?x=1&y=%2f#f" + ); + let invalid = super::effective_document_base( + br#""#, + &document, + ) + .expect("invalid first base falls back"); + assert_eq!(invalid, document); + } + + #[test] + fn decoded_query_bytes_are_serialized_once_in_each_rehosted_attribute() { + let value = "https://cdn.example.net/npm/a.png?q=%2f&literal=&amp;#f%2F"; + let page = rendered( + &rehosting_settings(), + &format!( + r#""# + ), + ); + let emitted = format!( + "https://{REQUEST_HOST}{PAGE_PREFIX}{ROUTE}a.png?q=%2f&literal=&amp;#f%2F" + ); + assert!(page.contains(&format!(r#"src="{emitted}""#)), "{page}"); + assert!( + page.contains(&format!(r#"srcset="{emitted} 1x""#)), + "{page}" + ); + assert!( + page.contains(&format!(r#"imagesrcset="{emitted} 2x""#)), + "{page}" + ); + } +} diff --git a/crates/trusted-server-core/src/html_processor/source_protection.rs b/crates/trusted-server-core/src/html_processor/source_protection.rs new file mode 100644 index 0000000000..7e926e0ad7 --- /dev/null +++ b/crates/trusted-server-core/src/html_processor/source_protection.rs @@ -0,0 +1,396 @@ +//! Bounded original-source hold around the entire HTML transformation pass. +//! Classification belongs to the selected middleware, not to core. + +use std::io; +use std::ops::Range; + +use futures::future::LocalBoxFuture; + +use crate::middleware::{MiddlewareChain, SourceProtection}; +use crate::platform::RuntimeServices; +use crate::streaming_processor::StreamProcessor; + +pub(super) struct SourceProtected

{ + inner: P, + classifiers: Vec, + original: Vec, + limit: usize, + ended: bool, +} + +impl

SourceProtected

{ + pub(super) fn new(inner: P, chains: &[&MiddlewareChain], limit: usize) -> Self { + Self { + inner, + classifiers: chains + .iter() + .flat_map(|chain| chain.source_protections()) + .collect(), + original: Vec::new(), + limit, + ended: false, + } + } + + fn hold(&mut self, chunk: &[u8]) -> io::Result<()> { + if self.ended { + return Err(refuse("source protection processor already ended")); + } + append(&mut self.original, chunk, self.limit) + } + + fn mask(&mut self) -> io::Result { + self.ended = true; + let mut ranges: Vec> = Vec::new(); + for classify in &self.classifiers { + let classified = classify(&self.original)?; + let mut previous_end = 0; + for range in &classified { + if range.start < previous_end + || range.start >= range.end + || range.end > self.original.len() + { + return Err(refuse("source protection returned invalid ranges")); + } + previous_end = range.end; + } + ranges.extend(classified); + } + ranges.sort_unstable_by_key(|range| (range.start, range.end)); + let mut merged: Vec> = Vec::with_capacity(ranges.len()); + for range in ranges { + if let Some(last) = merged.last_mut().filter(|last| range.start <= last.end) { + last.end = last.end.max(range.end); + } else { + merged.push(range); + } + } + MaskedDocument::new(std::mem::take(&mut self.original), merged, self.limit) + } +} + +impl StreamProcessor for SourceProtected

{ + fn process_chunk(&mut self, chunk: &[u8], is_last: bool) -> io::Result> { + if self.classifiers.is_empty() { + return self.inner.process_chunk(chunk, is_last); + } + self.hold(chunk)?; + if !is_last { + return Ok(Vec::new()); + } + let masked = self.mask()?; + let input = masked.input(self.limit)?; + let output = self.inner.process_chunk(&input, true)?; + masked.restore(output, self.limit) + } + + fn finish<'a>( + &'a mut self, + chunk: &'a [u8], + services: &'a RuntimeServices, + ) -> LocalBoxFuture<'a, io::Result>> { + Box::pin(async move { + if self.classifiers.is_empty() { + return self.inner.finish(chunk, services).await; + } + self.hold(chunk)?; + let masked = self.mask()?; + let input = masked.input(self.limit)?; + let output = self.inner.finish(&input, services).await?; + masked.restore(output, self.limit) + }) + } + + fn reset(&mut self) { + self.inner.reset(); + self.original.clear(); + self.ended = false; + } +} + +struct MaskedDocument { + // Retain one original allocation. Protected SVGs remain slices, never copies. + original: Vec, + ranges: Vec>, + prefix: String, +} + +impl MaskedDocument { + fn new(original: Vec, ranges: Vec>, limit: usize) -> io::Result { + if original.len() > limit { + return Err(refuse("source protection original exceeds body limit")); + } + let prefix = if ranges.is_empty() { + String::new() + } else { + format!("", self.prefix) + } + + fn input(&self, limit: usize) -> io::Result> { + if self.ranges.is_empty() { + return Ok(std::borrow::Cow::Borrowed(&self.original)); + } + let mut input = Vec::new(); + let mut cursor = 0; + for (index, range) in self.ranges.iter().enumerate() { + append(&mut input, &self.original[cursor..range.start], limit)?; + append(&mut input, self.marker(index).as_bytes(), limit)?; + cursor = range.end; + } + append(&mut input, &self.original[cursor..], limit)?; + Ok(std::borrow::Cow::Owned(input)) + } + + fn restore(&self, output: Vec, limit: usize) -> io::Result> { + if output.len() > limit { + return Err(refuse("source protection output exceeds body limit")); + } + if self.ranges.is_empty() { + return Ok(output); + } + let mut restored = Vec::new(); + let mut cursor = 0; + for (index, range) in self.ranges.iter().enumerate() { + let at = find(&output[cursor..], self.prefix.as_bytes()) + .map(|at| cursor + at) + .ok_or_else(|| refuse("source protection marker lost"))?; + let marker = self.marker(index); + if !output[at..].starts_with(marker.as_bytes()) { + return Err(refuse( + "source protection marker duplicated, reordered or altered", + )); + } + append(&mut restored, &output[cursor..at], limit)?; + append(&mut restored, &self.original[range.clone()], limit)?; + cursor = at + marker.len(); + } + if find(&output[cursor..], self.prefix.as_bytes()).is_some() { + return Err(refuse("source protection marker duplicated")); + } + append(&mut restored, &output[cursor..], limit)?; + Ok(restored) + } +} + +fn append(output: &mut Vec, bytes: &[u8], limit: usize) -> io::Result<()> { + if bytes.len() > limit.saturating_sub(output.len()) { + return Err(refuse("source protection exceeds body limit")); + } + // Reserve explicitly so Vec's implicit growth cannot overshoot the bound. + let needed = output.len() + bytes.len(); + if needed > output.capacity() { + let capacity = output.capacity().saturating_mul(2).max(needed).min(limit); + output.reserve_exact(capacity - output.len()); + } + output.extend_from_slice(bytes); + Ok(()) +} + +fn find(haystack: &[u8], needle: &[u8]) -> Option { + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +fn refuse(message: &'static str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::middleware::{ + HTML_MEDIA_TYPE, Middleware, MiddlewareAction, MiddlewareContext, MiddlewarePhase, + }; + use crate::platform::test_support::noop_services; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct Policy(SourceProtection); + + impl Middleware for Policy { + fn middleware_id(&self) -> &'static str { + "test.protection" + } + fn phases(&self) -> &[MiddlewarePhase] { + &MiddlewarePhase::ALL + } + fn source_protection(&self) -> Option { + Some(self.0) + } + fn create(&self, _: &MiddlewareContext<'_>) -> MiddlewareAction { + MiddlewareAction::pass() + } + } + + struct Replace { + calls: Arc, + transform: fn(&[u8]) -> Vec, + } + + impl StreamProcessor for Replace { + fn process_chunk(&mut self, chunk: &[u8], _: bool) -> io::Result> { + self.calls.fetch_add(1, Ordering::Relaxed); + Ok((self.transform)(chunk)) + } + } + + fn processor( + classify: SourceProtection, + transform: fn(&[u8]) -> Vec, + limit: usize, + ) -> (SourceProtected, Arc) { + let chain = MiddlewareChain::new( + MiddlewarePhase::Serve, + HTML_MEDIA_TYPE, + vec![Arc::new(Policy(classify))], + ); + let calls = Arc::new(AtomicUsize::new(0)); + ( + SourceProtected::new( + Replace { + calls: Arc::clone(&calls), + transform, + }, + &[&chain], + limit, + ), + calls, + ) + } + + fn middle(_: &[u8]) -> io::Result>> { + Ok(std::iter::once(1..5).collect()) + } + fn unchanged(bytes: &[u8]) -> Vec { + bytes.to_vec() + } + + #[test] + fn holds_original_until_complete_and_restores_exact_bytes_after_async_finish() { + let (mut processor, calls) = processor( + middle, + |bytes| { + let mut output = b"outside".to_vec(); + output.extend_from_slice(&bytes[1..]); + output + }, + 1024, + ); + assert!( + processor + .process_chunk(b"aKEEP", false) + .expect("hold") + .is_empty() + ); + assert_eq!(calls.load(Ordering::Relaxed), 0, "no early transform"); + let result = + futures::executor::block_on(processor.finish(b"z", &noop_services())).expect("restore"); + assert_eq!(result, b"outsideKEEPz"); + assert_eq!(calls.load(Ordering::Relaxed), 1); + } + + #[test] + fn source_limit_is_checked_before_copying_or_running_transforms() { + let (mut processor, calls) = processor(middle, unchanged, 5); + assert!( + processor + .process_chunk(b"a", false) + .expect("hold") + .is_empty() + ); + let before = processor.original.capacity(); + assert!(processor.process_chunk(&[b'x'; 128], false).is_err()); + assert_eq!(processor.original, b"a"); + assert_eq!(processor.original.capacity(), before); + assert_eq!(calls.load(Ordering::Relaxed), 0); + } + + #[test] + fn refuses_invalid_ranges_without_exposing_output() { + for classify in [ + (|_: &[u8]| Ok(std::iter::once(4..7).collect())) as SourceProtection, + |_: &[u8]| Ok(std::iter::once(2..2).collect()), + |_: &[u8]| Ok(vec![1..4, 3..5]), + |_: &[u8]| Ok(vec![3..5, 0..1]), + |_: &[u8]| Err(refuse("classification failed")), + ] { + let (mut processor, calls) = processor(classify, unchanged, 1024); + assert!(processor.process_chunk(b"aKEEPz", true).is_err()); + assert_eq!(calls.load(Ordering::Relaxed), 0); + } + } + + #[test] + fn refuses_lost_duplicated_reordered_and_altered_markers() { + let masked = + MaskedDocument::new(b"aKEEPzOTHERq".to_vec(), vec![1..5, 6..11], 1024).expect("mask"); + let first = masked.marker(0); + let second = masked.marker(1); + for invalid in [ + format!("a{second}q"), + format!("a{first}{first}{second}q"), + format!("a{second}{first}q"), + format!("a{}x-->{second}q", masked.prefix), + format!("a{first}{second}{second}q"), + ] { + assert!(masked.restore(invalid.into_bytes(), 1024).is_err()); + } + assert_eq!( + masked + .restore(format!("edited{first}z{second}q").into_bytes(), 1024) + .expect("restore"), + b"editedKEEPzOTHERq" + ); + } + + #[test] + fn refuses_mask_and_restoration_expansion_past_limit() { + let short = MaskedDocument::new(b"aKEEPz".to_vec(), std::iter::once(1..5).collect(), 6) + .expect("original fits"); + assert!(short.input(6).is_err(), "opaque marker must fit too"); + let original = format!("a{}z", "K".repeat(128)).into_bytes(); + let limit = original.len(); + let masked = MaskedDocument::new( + original.clone(), + std::iter::once(1..limit - 1).collect(), + limit, + ) + .expect("fits"); + let input = masked.input(limit).expect("mask fits").into_owned(); + assert!(masked.restore(input.clone(), limit - 1).is_err()); + assert_eq!(masked.restore(input, limit).expect("exact limit"), original); + } + + #[test] + fn policy_is_default_off_and_selected_only_from_supplied_chains() { + let chain = MiddlewareChain::new(MiddlewarePhase::Serve, HTML_MEDIA_TYPE, Vec::new()); + let calls = Arc::new(AtomicUsize::new(0)); + let mut processor = SourceProtected::new( + Replace { + calls: Arc::clone(&calls), + transform: unchanged, + }, + &[&chain], + 1, + ); + assert_eq!( + processor.process_chunk(b"unheld", false).expect("stream"), + b"unheld" + ); + assert_eq!(calls.load(Ordering::Relaxed), 1); + } +} diff --git a/crates/trusted-server-core/src/inspect/config/tests.rs b/crates/trusted-server-core/src/inspect/config/tests.rs index c524504b8c..e6ad41944e 100644 --- a/crates/trusted-server-core/src/inspect/config/tests.rs +++ b/crates/trusted-server-core/src/inspect/config/tests.rs @@ -966,8 +966,6 @@ fn the_page_and_the_json_carry_the_view() { &request(CONFIG_JSON_PATH), ))) .expect("a JSON body"); - let keys: Vec<&String> = json.as_object().expect("an object").keys().collect(); - assert_eq!(keys, ["masked", "settings", "version"]); assert_eq!(json["version"], env!("CARGO_PKG_VERSION")); assert_eq!( json["settings"]["publisher"]["domain"], @@ -982,20 +980,6 @@ fn the_page_and_the_json_carry_the_view() { ); } -fn assert_keys_sorted(value: &Value) { - match value { - Value::Object(map) => { - let keys: Vec<&String> = map.keys().collect(); - let mut sorted = keys.clone(); - sorted.sort_unstable(); - assert_eq!(keys, sorted, "keys should be sorted"); - map.values().for_each(assert_keys_sorted); - } - Value::Array(items) => items.iter().for_each(assert_keys_sorted), - _ => {} - } -} - /// Two loads of one document build hash maps of their own, each iterating /// in its own order, and the view has to come out the same. #[test] @@ -1015,7 +999,6 @@ fn the_same_settings_give_the_same_view() { assert_eq!(first_json, second_json); let parsed: Value = serde_json::from_str(&first_json).expect("JSON"); - assert_keys_sorted(&parsed); assert_eq!( parsed["settings"]["response_headers"] .as_object() diff --git a/crates/trusted-server-core/src/integrations/js_asset_proxy.rs b/crates/trusted-server-core/src/integrations/js_asset_proxy.rs index 69d9fca9ab..2e9b90a296 100644 --- a/crates/trusted-server-core/src/integrations/js_asset_proxy.rs +++ b/crates/trusted-server-core/src/integrations/js_asset_proxy.rs @@ -684,9 +684,13 @@ mod tests { vec![Arc::new(ScriptAddress(integration))], ); let config = HtmlProcessorConfig { + rehost: crate::rehost::PageRehost::default(), csp_nonce_observed: None, + response_headers: None, public_prefix: None, prepared: std::sync::Arc::new(crate::middleware::Prepared::default()), + status: http::StatusCode::OK, + origin_private: false, body_close: BodyCloseInjection::None, origin_host: "origin.example.com".to_string(), request_host: "publisher.example.com".to_string(), diff --git a/crates/trusted-server-core/src/integrations/registry.rs b/crates/trusted-server-core/src/integrations/registry.rs index e57a587070..8d7fb137bb 100644 --- a/crates/trusted-server-core/src/integrations/registry.rs +++ b/crates/trusted-server-core/src/integrations/registry.rs @@ -22,6 +22,7 @@ use crate::error::TrustedServerError; use crate::evidence::OwnedRequestInfo; use crate::geo::GeoInfo; use crate::http_util::is_navigation_request; +use crate::media_signing::MediaSigner; use crate::middleware::{Middleware, MiddlewareChain, MiddlewarePhase, PhaseEntries}; use crate::module_context::{ModuleCall, ModuleContext, ModuleRequest, ResolvedRequest}; use crate::origin_auth::OriginSigners; @@ -195,15 +196,23 @@ impl IntegrationDocumentState { /// response with a body, or another response a `[[serve]]` entry covers, is /// sent `private, no-store`. A fetch middleware is handed none of these /// values. +/// +/// An observation, left with [`IntegrationRequestState::observe`], is read +/// the same way but changes nothing about how the request is answered: a +/// request carrying only observations keeps the shared template and media +/// paths every reader takes. It is for a module that has to see, not +/// change, what readers are served. #[derive(Clone, Default)] pub struct IntegrationRequestState { values: IntegrationDocumentStateMap, + observed: IntegrationDocumentStateMap, } impl std::fmt::Debug for IntegrationRequestState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("IntegrationRequestState") .field("keys", &self.values.keys().collect::>()) + .field("observed", &self.observed.keys().collect::>()) .finish() } } @@ -244,36 +253,55 @@ impl IntegrationRequestState { .unwrap_or_default() } - /// The value of type `T` left for `integration_id`. + /// Leaves `value` on `request` as an observation of `integration_id`, + /// which reads like a value but leaves the request on every reader's + /// path, see [`IntegrationRequestState`]. + pub fn observe(request: &mut Request, integration_id: &'static str, value: T) + where + T: Any + Send + Sync + 'static, + { + let mut state = request + .extensions_mut() + .remove::() + .unwrap_or_default(); + state + .observed + .insert((integration_id, TypeId::of::()), Arc::new(value)); + request.extensions_mut().insert(state); + } + + /// The value or observation of type `T` left for `integration_id`. #[must_use] pub fn get(&self, integration_id: &'static str) -> Option> where T: Any + Send + Sync + 'static, { - let value = self.values.get(&(integration_id, TypeId::of::()))?; + let key = (integration_id, TypeId::of::()); + let value = self.values.get(&key).or_else(|| self.observed.get(&key))?; Arc::clone(value).downcast::().ok() } - /// Whether no module left a value. + /// Whether no module left a value. Observations do not count. #[must_use] pub fn is_empty(&self) -> bool { self.values.is_empty() } - /// Copies every value into the state of a document that is starting. + /// Copies every value and observation into the state of a document that + /// is starting. /// /// # Panics /// /// Panics if the document state's lock is poisoned. pub(crate) fn seed(&self, document: &IntegrationDocumentState) { - if self.values.is_empty() { + if self.values.is_empty() && self.observed.is_empty() { return; } let mut guard = document .inner .lock() .expect("should lock integration document state"); - for (key, value) in &self.values { + for (key, value) in self.observed.iter().chain(&self.values) { guard.insert(*key, Arc::clone(value)); } } @@ -709,6 +737,11 @@ pub struct IntegrationRegistration { /// on every request, so declaring one runs it wherever the registration /// itself runs. pub identifier_module: Option>, + /// Media signer this registration supplies, see [`crate::media_signing`]. + /// + /// No section selects one. It runs wherever the registration itself + /// runs, and at most one running registration may supply one. + pub media_signer: Option>, /// Asset cache module this registration supplies, under its name. /// /// Taken only from the registration of the builder `[asset-cache]` @@ -748,6 +781,7 @@ impl IntegrationRegistrationBuilder { source_module: None, analytics_module: None, identifier_module: None, + media_signer: None, asset_cache_module: None, }, } @@ -880,6 +914,18 @@ impl IntegrationRegistrationBuilder { self } + /// Declare the media signer this registration supplies, which core asks + /// to sign the binary responses it claims, see [`crate::media_signing`]. + /// + /// It runs wherever the registration runs. The registry refuses to build + /// when two running registrations supply one, because signing a body + /// twice would sign the first signer's output as a source. + #[must_use] + pub fn with_media_signer(mut self, signer: Arc) -> Self { + self.registration.media_signer = Some(signer); + self + } + /// Declare the rules this registration adds to `/robots.txt`, under their /// `name`. /// @@ -1049,6 +1095,9 @@ struct IntegrationRegistryInner { // registration order, which core asks on every request. Shared, so a // request's services take a reference count rather than a copy. identifier_modules: Arc<[Arc]>, + // The media signer a running registration supplies, with the + // integration that supplied it. At most one. + media_signer: Option<(&'static str, Arc)>, // The asset cache module of the builder `[asset-cache]` selects, or // `None` when it selects none. asset_cache_module: Option>, @@ -1101,6 +1150,7 @@ impl Default for IntegrationRegistryInner { analytics_module: None, sources: None, identifier_modules: Arc::default(), + media_signer: None, asset_cache_module: None, } } @@ -2429,6 +2479,19 @@ impl IntegrationRegistry { )?; identifier_modules.push(module); } + if let Some(signer) = registration.media_signer { + if let Some((supplier, _)) = &inner.media_signer { + return Err(Report::new(TrustedServerError::Configuration { + message: format!( + "integrations `{supplier}` and `{}` both supply a media signer, and \ + at most one running integration may, because a body is signed \ + once. Select only one of their modules", + registration.integration_id + ), + })); + } + inner.media_signer = Some((registration.integration_id, signer)); + } if let Some((name, contributor)) = registration.robots_txt_contributor { claim_module_name( &mut claimed, @@ -2611,6 +2674,17 @@ impl IntegrationRegistry { Arc::clone(&self.inner.identifier_modules) } + /// The media signer the running registrations supply, if one does. + /// Adapters hand it to each request's services with + /// [`RuntimeServices::with_media_signer`]. + #[must_use] + pub fn media_signer(&self) -> Option> { + self.inner + .media_signer + .as_ref() + .map(|(_, signer)| Arc::clone(signer)) + } + /// The prefixes of the publisher `request` was resolved to under /// `settings`, from the selected prefix module. /// @@ -3219,6 +3293,7 @@ impl IntegrationRegistry { analytics_module: None, sources: None, identifier_modules: Arc::default(), + media_signer: None, asset_cache_module: None, }), plan: None, @@ -3308,6 +3383,7 @@ impl IntegrationRegistry { analytics_module: None, sources: None, identifier_modules: Arc::default(), + media_signer: None, asset_cache_module: None, }), plan: None, @@ -4232,6 +4308,35 @@ pub(crate) mod test_support { } } + /// Ends each stylesheet a `[[serve]]` entry names it for with a line, + /// standing in for a stylesheet signer. + pub(crate) const STYLESHEET: &str = "testing.middleware-fixture.stylesheet"; + /// The line [`STYLESHEET`] adds. + pub(crate) const STYLESHEET_LINE: &str = "/* served by the middleware fixture */\n"; + + struct Stylesheet; + + impl Middleware for Stylesheet { + fn middleware_id(&self) -> &'static str { + STYLESHEET + } + + fn handles_media_type(&self, media_type: &str) -> bool { + media_type == "text/css" + } + + fn phases(&self) -> &[MiddlewarePhase] { + &[MiddlewarePhase::Serve] + } + + fn create(&self, _context: &MiddlewareContext<'_>) -> MiddlewareAction { + MiddlewareAction { + stream: Some(Box::new(AppendLine(STYLESHEET_LINE))), + ..MiddlewareAction::pass() + } + } + } + /// Says it moves the origin's address itself, on its own media type, /// and then leaves the body as it came with a line added, so a test /// sees what core left to it. @@ -4311,6 +4416,7 @@ pub(crate) mod test_support { .with_middleware(Arc::new(Locating)) .with_middleware(Arc::new(Labeled { text: labeled })) .with_middleware(Arc::new(Framed)) + .with_middleware(Arc::new(Stylesheet)) .build(), )) } @@ -4714,6 +4820,7 @@ mod tests { }; use super::*; use crate::constants::COOKIE_EC; + use crate::media_signing::test_support::{Behavior, StubSigner}; use crate::middleware::{HTML_MEDIA_TYPE, MiddlewareAction, MiddlewareContext, PhaseEntry}; use crate::permissions::{Permission, PermissionSet, PermissionState}; use crate::platform::test_support::noop_services; @@ -7964,6 +8071,101 @@ mod tests { ); } + /// The names the two media signer test modules run under. + const MEDIA_PROBE_MODULE: &str = "testing.media-probe"; + const MEDIA_SECOND_MODULE: &str = "testing.media-second"; + + fn media_probe_registration( + _settings: &Settings, + ) -> Result, Report> { + Ok(Some( + IntegrationRegistration::builder("media-probe") + .with_media_signer(StubSigner::new(Behavior::Sign, 1024)) + .build(), + )) + } + + fn media_second_registration( + _settings: &Settings, + ) -> Result, Report> { + Ok(Some( + IntegrationRegistration::builder("media-second") + .with_media_signer(StubSigner::new(Behavior::Sign, 1024)) + .build(), + )) + } + + fn media_probe_builder() -> crate::integrations::IntegrationBuilder { + crate::integrations::IntegrationBuilder::new( + "media-probe", + "seam-probe", + media_probe_registration, + validate_nothing, + ) + .with_module_name(MEDIA_PROBE_MODULE) + } + + /// No section selects a media signer. A running registration's is handed + /// out for each request's services. + #[test] + fn a_running_registration_hands_out_its_media_signer() { + let settings = settings_naming(MEDIA_PROBE_MODULE); + + let registry = IntegrationRegistry::with_registrations(&settings, &[media_probe_builder()]) + .expect("should build a registry with a media signer"); + + assert_eq!( + registry.media_signer().map(|signer| signer.signer_id()), + Some("testing.media"), + "should hand out the running registration's signer" + ); + } + + /// A registration a section does not select runs nothing, its media + /// signer included, so no response is claimed. + #[test] + fn an_unselected_registration_offers_no_media_signer() { + let settings = crate::test_support::tests::create_test_settings(); + + let registry = IntegrationRegistry::with_registrations(&settings, &[media_probe_builder()]) + .expect("should build a registry without the probe selected"); + + assert!( + registry.media_signer().is_none(), + "an unselected registration's signer should not be handed out" + ); + } + + /// Two running signers would sign one signer's output as the other's + /// source, so the pair is refused. + #[test] + fn two_running_registrations_supplying_a_media_signer_are_refused() { + let mut settings = settings_naming(MEDIA_PROBE_MODULE); + settings.select_module("testing", MEDIA_SECOND_MODULE); + let extra = [ + media_probe_builder(), + crate::integrations::IntegrationBuilder::new( + "media-second", + "seam-probe", + media_second_registration, + validate_nothing, + ) + .with_module_name(MEDIA_SECOND_MODULE), + ]; + + let error = IntegrationRegistry::with_registrations(&settings, &extra) + .err() + .expect("should refuse two media signers"); + + let message = error.to_string(); + assert!( + message.contains("both supply a media signer") + && message.contains("`media-probe`") + && message.contains("`media-second`"), + "error should name both integrations: {message}" + ); + } + fn html_entry(path: Option<&str>, names: &[&str]) -> PhaseEntry { PhaseEntry { media_type: HTML_MEDIA_TYPE.to_owned(), @@ -7989,51 +8191,6 @@ mod tests { let registry = IntegrationRegistry::new(&settings) .expect("should build a registry whose entries name registered middleware"); - assert_eq!( - registry.middleware_ids(), - [ - fixture::HEAD, - fixture::LINKS, - fixture::BROKEN, - fixture::READER, - fixture::BROKEN_READER, - fixture::TEXT, - fixture::PREPARING, - fixture::REFUSING, - fixture::TEXT_HEADER, - fixture::LOCATING, - fixture::LABELED, - fixture::FRAMED, - crate::service_version::MODULE, - ], - "should list the middleware the selected module supplies, and core's own after them" - ); - assert_eq!( - registry.middleware_in(MiddlewarePhase::Fetch), - [ - fixture::HEAD, - fixture::LINKS, - fixture::BROKEN, - fixture::TEXT, - fixture::PREPARING, - fixture::REFUSING, - fixture::LABELED, - fixture::FRAMED - ], - "should list the middleware that run in the fetch phase" - ); - assert_eq!( - registry.middleware_in(MiddlewarePhase::Serve), - [ - fixture::READER, - fixture::BROKEN_READER, - fixture::PREPARING, - fixture::TEXT_HEADER, - fixture::LOCATING, - crate::service_version::MODULE - ], - "should list the middleware that run in the serve phase, core's own last" - ); let chain_for = |media_type: &str, path: &str| { registry .middleware_chain(&settings.fetch, MiddlewarePhase::Fetch, media_type, path) @@ -8256,45 +8413,6 @@ mod tests { ); } - #[test] - fn an_entry_naming_a_middleware_nothing_supplies_is_refused_with_what_is_supplied() { - let settings = fixture_settings_with_entries(vec![ - html_entry(Some("/news/"), &[fixture::HEAD]), - html_entry(None, &[fixture::HEAD, "testing.nothing"]), - ]); - - let error = IntegrationRegistry::new(&settings) - .err() - .expect("should refuse an entry naming a middleware nothing supplies"); - - let message = error.to_string(); - assert!( - message.contains( - "[[fetch]] entry 2 names `testing.nothing`, which no module that runs supplies" - ) && message.contains(&format!( - "[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", - fixture::HEAD, - fixture::LINKS, - fixture::BROKEN, - fixture::READER, - fixture::BROKEN_READER, - fixture::TEXT, - fixture::PREPARING, - fixture::REFUSING, - fixture::TEXT_HEADER, - fixture::LOCATING, - fixture::LABELED, - fixture::FRAMED, - crate::service_version::MODULE - )), - "should name the entry and list what could be named: {message}" - ); - assert!( - !message.contains("no section selects"), - "should not blame a selection when the name is no module's: {message}" - ); - } - #[test] fn an_entry_naming_the_middleware_of_a_module_no_section_selects_says_so() { let mut settings = crate::test_support::tests::create_test_settings(); @@ -8436,59 +8554,6 @@ mod tests { ); } - #[test] - fn a_middleware_no_entry_names_is_listed_for_the_startup_warning() { - let unnamed = |entries: Vec| -> Vec<&'static str> { - let settings = fixture_settings_with_entries(entries); - let registry = IntegrationRegistry::new(&settings).expect("should build a registry"); - unnamed_middleware(&settings, ®istry.inner) - .into_iter() - .map(|(integration, middleware)| { - assert_eq!(integration, fixture::ID, "should name the supplier"); - middleware.middleware_id() - }) - .collect() - }; - - assert_eq!( - unnamed(Vec::new()), - [ - fixture::HEAD, - fixture::LINKS, - fixture::BROKEN, - fixture::READER, - fixture::BROKEN_READER, - fixture::TEXT, - fixture::PREPARING, - fixture::REFUSING, - fixture::TEXT_HEADER, - fixture::LOCATING, - fixture::LABELED, - fixture::FRAMED, - ], - "should list every middleware when there are no entries" - ); - assert_eq!( - unnamed(vec![ - html_entry(Some("/news/"), &[fixture::LINKS]), - html_entry(None, &[fixture::HEAD]), - ]), - [ - fixture::BROKEN, - fixture::READER, - fixture::BROKEN_READER, - fixture::TEXT, - fixture::PREPARING, - fixture::REFUSING, - fixture::TEXT_HEADER, - fixture::LOCATING, - fixture::LABELED, - fixture::FRAMED - ], - "should leave out a middleware any entry names" - ); - } - #[test] fn an_entry_naming_a_middleware_in_a_phase_it_does_not_run_in_is_refused() { let mut settings = settings_naming(fixture::MODULE); @@ -8541,26 +8606,6 @@ mod tests { chain_for(MiddlewarePhase::Serve, "/sport/today").is_empty(), "should run no serve middleware on a path only the fetch entries cover" ); - let unnamed: Vec<&str> = unnamed_middleware(&settings, ®istry.inner) - .into_iter() - .map(|(_, middleware)| middleware.middleware_id()) - .collect(); - assert_eq!( - unnamed, - [ - fixture::LINKS, - fixture::BROKEN, - fixture::BROKEN_READER, - fixture::TEXT, - fixture::PREPARING, - fixture::REFUSING, - fixture::TEXT_HEADER, - fixture::LOCATING, - fixture::LABELED, - fixture::FRAMED - ], - "should count a middleware as named only by an entry of a phase it runs in" - ); } /// A middleware under a name of the test's choosing, running in the diff --git a/crates/trusted-server-core/src/lib.rs b/crates/trusted-server-core/src/lib.rs index 26d98f091c..7605994daf 100644 --- a/crates/trusted-server-core/src/lib.rs +++ b/crates/trusted-server-core/src/lib.rs @@ -43,6 +43,7 @@ pub mod constants; pub mod cookies; pub mod creative; pub mod creative_opportunities; +pub(crate) mod css_relocation; pub mod ec; pub mod edge_cookie; pub mod error; @@ -54,6 +55,7 @@ pub mod html_processor; pub mod http_util; pub mod inspect; pub mod integrations; +pub mod media_signing; pub mod middleware; pub mod module_context; pub mod module_name; @@ -68,6 +70,7 @@ pub mod provider_table; pub mod proxy; pub mod publisher; pub mod redacted; +pub mod rehost; pub mod request_signing; pub(crate) mod response_header_rewrite; pub mod response_privacy; diff --git a/crates/trusted-server-core/src/media_signing.rs b/crates/trusted-server-core/src/media_signing.rs new file mode 100644 index 0000000000..8e43e877a5 --- /dev/null +++ b/crates/trusted-server-core/src/media_signing.rs @@ -0,0 +1,2295 @@ +//! Signed media, which a module makes from the exact bytes of a binary +//! response on its way to a reader. +//! +//! A module supplies a [`MediaSigner`] through +//! [`IntegrationRegistrationBuilder::with_media_signer`](crate::integrations::IntegrationRegistrationBuilder::with_media_signer), +//! and it runs wherever the registration runs. Core asks it about two +//! paths: a binary response the publisher path passes through, and the +//! answer of an asset route. Which responses are signed is decided by the +//! origin's media type, so a path with no file extension is covered as an +//! image, audio or video file is. +//! +//! The origin is asked as the reader asked, so a response that is not +//! signed keeps the origin's own range and conditional handling. Where the +//! origin answers `200` with a type the signer handles, within its limit, +//! core captures the body before anything else reads it and hands those +//! exact bytes to the signer. A request that plausibly asks for media asks +//! the origin for an identity-coded body, the only coding signed. Where the +//! origin answers a `206` or a `HEAD` for such a type, core asks it once +//! more for the whole representation, unconditionally, and signs what that +//! answer carries, but only where the signed whole can be kept, so the +//! origin is not read whole for every range. Where the reader's range or +//! `If-Match` brings a `416` or `412` the longer signed representation may +//! answer, and where the reader's `If-Range` or `If-Match` names a signed +//! representation, the origin is asked for the whole representation too. +//! Core applies no transformation of its own, so the captured source is +//! also the final body. +//! +//! An asset route that uses the platform's image optimizer is the one +//! place a transformation happens, and the optimizer reads the origin +//! itself, so core never holds its input. Core asks the optimizer to keep +//! its input's C2PA manifest and add a manifest of its own recording what +//! it did, which binds the input it read by hash. Only a whole `200` to a +//! `GET` whose body carries a C2PA manifest store is handed to the signer, +//! marked as [`MediaJob::optimizer_output`], so the optimizer's own record +//! is the parent of what is signed and core adds nothing to the history. +//! An optimizer writes such a record only where its input carried a +//! manifest, so an unsigned original's optimized answer is never signed. +//! A route may instead send the optimizer to its own signed original on +//! the serving host, see `AssetImageOptimizerConfig::signed_source`, so +//! the optimizer's input is the exact origin bytes core captured and +//! signed. +//! Nothing of it is kept, since its bytes depend on the transformation and +//! the reader's `Accept`, and its signed answer says it varies by `Accept`. +//! +//! The signed bytes are a representation of their own. Core gives them a +//! strong `ETag` of their own digest, a `Repr-Digest` and a +//! `Content-Length`, and answers the reader's `If-Match`, `If-None-Match`, +//! `If-Range` and `Range` against them, with `304`, `412`, `206` and `416` +//! as each requires. The origin's validators never describe signed bytes, +//! so they are removed. Ranges are offered only on a signed representation +//! that was kept. +//! +//! A signed representation is kept in the `[cache.assets]` loading cache +//! for as long as the origin's own headers made it shareably fresh, under +//! the namespace the signer signed it under, and only for a request whose +//! answer the origin could not have made one reader's own. Before the +//! origin is asked, a request of that kind which plausibly asks for media +//! and asks for no revalidation is looked up under the signer's current +//! [`namespace`](MediaSigner::namespace), so a changed or revoked policy +//! never serves a kept copy. A kept copy carries its `Age`, and one whose +//! body no longer hashes to its tag is dropped. +//! +//! What is never signed: a response the origin marked as one reader's, a +//! request carrying `Authorization`, a body larger than the signer's limit, +//! a content-coded body, a body the origin forbade transforming, a +//! response whose own `Link` already names a C2PA manifest, read from the +//! origin's own headers before core writes a cache policy, and a captured +//! body that is an init or media segment of a fragmented ISO BMFF stream, +//! whose provenance binds the stream's parts together. Such a response +//! goes to the reader as the origin answered it, its `Link` untouched. +//! One core meant to +//! sign and could not goes out without the origin's validators and held +//! only briefly and privately, so the next request can be signed. +//! +//! A signer may also sign a source larger than its limit, unchanged, in +//! the large lane, see [`MediaSigner::sign_large`]. Core never holds such a +//! source whole: it pins one version by the origin's strong `ETag` and +//! length, hashes it in bounded chunks, hands the signer a +//! [`VersionedMediaSource`] whose every read is pinned with `If-Match`, and +//! answers the reader with the origin's own bytes and validators plus the +//! signer's headers and a `Repr-Digest`. Only metadata is kept, under the +//! signer's current namespace and the pinned tag. A source without a strong +//! `ETag` goes to the reader as the origin answered it. + +use std::pin::Pin; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use base64::Engine as _; +use bytes::{Bytes, BytesMut}; +use edgezero_core::body::Body as EdgeBody; +use futures::StreamExt as _; +use http::{HeaderMap, HeaderName, HeaderValue, Method, Response, StatusCode, header}; +use sha2::{Digest as _, Sha256}; +use web_time::{SystemTime, UNIX_EPOCH}; + +use crate::integrations::IntegrationRequestState; +use crate::middleware::{ResponseHeader, media_type_of}; +use crate::platform::{CacheSpec, LoadError, LoadedValue, RuntimeServices, StoreKey}; +use crate::response_privacy::cache_control_forbids_shared_storage; +use crate::settings::Settings; + +mod large; + +pub(crate) use large::{LargeAnswer, PinnedOrigin}; +pub use large::{ + LargeMediaJob, LargeMediaSigning, LargeSignedMedia, MediaSourceError, SourceVersion, + VersionedMediaSource, +}; + +/// The loading cache a media signer declares for kept signed representations. +/// +/// A signer builder declares this feature through `with_cache_features`; +/// `[cache.assets]` selects its loading cache and lifetimes. Raw origin files +/// belong to the separate cache declared by the selected asset cache module. +pub const ASSETS_CACHE: &str = "assets"; + +const ASSET_DEFAULT_LIFETIMES: crate::platform::CacheLifetimes = crate::platform::CacheLifetimes { + memory: Duration::from_secs(5), + store: Duration::from_secs(60 * 60), + store_idle: Duration::from_secs(24 * 60 * 60), +}; + +/// A module that signs the exact bytes of a binary representation. +/// +/// Registered with +/// [`IntegrationRegistrationBuilder::with_media_signer`](crate::integrations::IntegrationRegistrationBuilder::with_media_signer). +/// At most one running registration may supply one. +pub trait MediaSigner: Send + Sync { + /// The module's name, for logs. + fn signer_id(&self) -> &'static str; + + /// Whether a `GET` or `HEAD` request for `path` may be signed, asked + /// before the origin is. Core changes nothing about the origin request, + /// so this only leaves out paths that never carry media, such as an API. + /// The media type of the origin's answer decides the rest, so a signer + /// answers `true` for a path with no file extension. + fn handles_path(&self, path: &str) -> bool; + + /// Whether this signer signs a body of `media_type`, a bare lower case + /// `type/subtype` read from the origin's `Content-Type`. + fn handles_media_type(&self, media_type: &str) -> bool; + + /// The largest body core captures for this signer. A larger one goes to + /// the reader as the origin answered it, never held whole. + fn max_source_bytes(&self) -> usize; + + /// The policy and credential context a kept signed representation is + /// bound to, from a policy check no older than the signer's own policy + /// lifetime. Core keeps a signed representation under it and serves a + /// kept one only while this answers the same value. An error keeps + /// nothing and serves nothing kept for the request. Never a secret. + fn namespace<'a>(&'a self, services: &'a RuntimeServices) -> MediaNamespacing<'a>; + + /// Signs one captured representation. + fn sign<'a>(&'a self, services: &'a RuntimeServices, job: &'a MediaJob<'a>) + -> MediaSigning<'a>; + + /// The largest source this signer signs unchanged in the large lane, + /// see [`sign_large`](Self::sign_large), or `0`, the default, where it + /// signs none. Core uses the lane only for a source larger than + /// [`max_source_bytes`](Self::max_source_bytes) and no larger than this. + fn max_large_source_bytes(&self) -> u64 { + 0 + } + + /// The one wall-clock budget for a large source: core's hashing of it + /// and the whole of [`sign_large`](Self::sign_large). Core drops the + /// work, stalled reads included, when it is spent. `Duration::ZERO`, + /// the default, keeps the large lane closed, and the lane is also + /// closed on a platform without a timer. + fn max_large_media_duration(&self) -> Duration { + Duration::ZERO + } + + /// Signs one large source that reaches the reader unchanged, from its + /// whole-source digest and pinned reads, returning only the headers its + /// answers carry. + /// + /// Core asks only for a shared `GET` whose origin answer is a `200` or + /// `206` of a type the signer handles, identity coded, not marked as + /// one reader's, not forbidden from transformation, naming no manifest + /// of its own, saying `Accept-Ranges: bytes`, with one strong `ETag` + /// and a known total length above + /// [`max_source_bytes`](Self::max_source_bytes) and within + /// [`max_large_source_bytes`](Self::max_large_source_bytes), on a + /// platform that streams response bodies and offers a timer, and only + /// where no metadata is kept for that version under the current + /// [`namespace`](Self::namespace). The default signs nothing. + fn sign_large<'a>( + &'a self, + _services: &'a RuntimeServices, + _job: &'a LargeMediaJob<'a>, + ) -> LargeMediaSigning<'a> { + Box::pin(async { + Err(MediaSigningFailure { + reason: "large_media_unsupported", + }) + }) + } + + /// Whether delivery only looks up results the signer already holds, so + /// core never runs a signing load for a reader. Core then keeps no + /// signed representation and reads none it kept, never starts or joins + /// a large-lane load, and asks [`lookup_large`](Self::lookup_large) for + /// a large version instead. [`sign`](Self::sign) is still asked for a + /// captured body, and must itself only look up. `false` by default. + fn lookup_only(&self) -> bool { + false + } + + /// The published headers of one large version, for a signer that + /// answers [`lookup_only`](Self::lookup_only). Core decorates the + /// origin's own answer with them where the length matches. The default + /// finds nothing. + fn lookup_large<'a>( + &'a self, + _services: &'a RuntimeServices, + _job: &'a LargeLookupJob<'a>, + ) -> LargeLookup<'a> { + Box::pin(async { + Err(MediaSigningFailure { + reason: "large_lookup_unsupported", + }) + }) + } + + /// Whether the request the module's hooks left `state` on reads a + /// representation for the signer's own preparation rather than for a + /// reader. Core then signs and looks up nothing, keeps nothing, and + /// sends the origin's answer as it came with + /// [`snapshot_headers`](Self::snapshot_headers) added. `false` by + /// default. + fn observes_snapshot(&self, _state: &IntegrationRequestState) -> bool { + false + } + + /// Headers written on the origin's answer to a request + /// [`observes_snapshot`](Self::observes_snapshot) admitted, reporting + /// what the origin's own headers said about it. None by default. + fn snapshot_headers( + &self, + _state: &IntegrationRequestState, + _facts: &SnapshotFacts, + ) -> Vec { + Vec::new() + } +} + +/// What [`MediaSigner::lookup_large`] returns. +pub type LargeLookup<'a> = + Pin> + 'a>>; + +/// One large version a lookup-only signer is asked about, named by the +/// origin's own validators. An entity tag is not cryptographic evidence: +/// the signer relies on the origin honoring strong validator semantics. +#[derive(Debug, Clone, Copy)] +pub struct LargeLookupJob<'a> { + /// The address the reader asked for, `scheme://host/path`, without its + /// query. + pub reader_url: &'a str, + /// The reader's actual query, without `?`, empty where absent. The + /// signer must admit it before using it as a source-version variant. + pub reader_query: &'a str, + /// What tells this origin apart from another at one address. + pub origin_identity: &'a str, + /// The lower case host the source is fetched from. + pub source_origin_host: &'a str, + /// The origin's `Content-Type`, as it sent it. + pub content_type: &'a str, + /// The single strong entity tag the origin named, quoted. + pub etag: &'a str, + /// The version's total length. + pub length: u64, +} + +/// A large version's published result: the headers its answers carry and +/// the whole-source digest and length it was verified at. +#[derive(Debug, Clone)] +pub struct PublishedLarge { + /// Headers written on every answer of the version, as + /// [`LargeSignedMedia::headers`]. + pub headers: Vec, + /// SHA-256 of every byte of the version, written as its `Repr-Digest`. + pub sha256: [u8; 32], + /// The version's length. + pub length: u64, +} + +/// What the origin's own headers said about the answer a preparation +/// snapshot read, read before core changed any of them. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SnapshotFacts { + /// The origin's status. + pub status: StatusCode, + /// The origin marked the answer as one reader's, by a cookie, a + /// `Cache-Control` that forbids shared storage or a `Vary` on the + /// reader. + pub private: bool, + /// The origin forbade transforming the body. + pub no_transform: bool, + /// The origin's own `Link` names a C2PA manifest. + pub manifest_link: bool, + /// The origin's `Content-Encoding`, empty for identity. + pub content_encoding: String, + /// The lower case host the source was fetched from. + pub origin_host: String, + /// Lowercase SHA-256 hex of core's configured-origin identity, + /// distinguishing configured sources even at the same host. + pub origin_identity_sha256: String, + /// The exact fetched address, where it carries no user information, + /// query or fragment. + pub source_reference: Option, +} + +/// What [`MediaSigner::sign`] returns. +pub type MediaSigning<'a> = + Pin> + 'a>>; + +/// What [`MediaSigner::namespace`] returns. +pub type MediaNamespacing<'a> = + Pin> + 'a>>; + +/// One captured representation a signer is asked to sign. +#[derive(Debug, Clone, Copy)] +pub struct MediaJob<'a> { + /// The address the reader asked for, `scheme://host/path`, without its + /// query. + pub reader_url: &'a str, + /// The reader's raw query, without `?`. A lookup-only signer admits it + /// through its configured variant schema before requesting preparation. + pub reader_query: &'a str, + /// The origin's `Content-Type`, as it sent it. + pub content_type: &'a str, + /// The exact body core received, captured before anything else read + /// it. Core transforms nothing, so it is also the final body: the + /// operations core performed are none and the record is complete. + pub source: &'a Bytes, + /// SHA-256 of [`source`](Self::source), lower case hex. + pub source_sha256: &'a str, + /// The address core fetched the source from, exactly as fetched, or + /// `None` when that address carries user information, a query or a + /// fragment, or when the source is + /// [`optimizer_output`](Self::optimizer_output). It names a mutable + /// resource, not an immutable version. + pub source_reference: Option<&'a str>, + /// The lower case host of the address core fetched the source from, + /// with no scheme, port, path, query or user information, present even + /// where [`source_reference`](Self::source_reference) is not. Core + /// always sets it for a body it captured over HTTP. For + /// [`optimizer_output`](Self::optimizer_output) it is the origin host + /// the optimizer was asked to read. + pub source_origin_host: Option<&'a str>, + /// The source is the platform image optimizer's answer, not the + /// origin's bytes. Core hands such a source over only where it carries + /// a C2PA manifest store, which the optimizer writes to record what it + /// did to its own input. A signer must have the signature refused + /// unless that embedded manifest is present and intact, so the + /// optimizer's own record of its input and transformations is the + /// parent of what is signed, never replaced by a claim of core's. + pub optimizer_output: bool, +} + +/// The signed representation a signer returns. +#[derive(Debug, Clone)] +pub struct SignedMedia { + /// The final signed bytes, which core sends as they are. + pub bytes: Bytes, + /// The `Content-Type` of the signed bytes. + pub content_type: HeaderValue, + /// Headers the signer writes on every response of the signed + /// representation: `200`, `206`, `304`, `412`, `416` and `HEAD`. A `Link` + /// is added to the origin's own links, and any other replaces the + /// origin's header of that name. A name core owns, see + /// [`is_reserved_header`], fails the signing. + pub headers: Vec, + /// The policy and credential context these bytes were actually signed + /// under, as [`MediaSigner::namespace`] gives it for that context. Core + /// keeps the signed representation under this value, never under a + /// namespace asked for again, so bytes signed under one policy are never + /// served while another is current. + pub namespace: String, +} + +/// Why a signer did not sign, which core logs before it sends the origin's +/// bytes unsigned. +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] +#[display("{reason}")] +pub struct MediaSigningFailure { + /// A short code naming the reason. + pub reason: &'static str, +} + +impl core::error::Error for MediaSigningFailure {} + +/// Headers core writes on a signed representation, which a signer may not +/// set. +const RESERVED_HEADERS: [&str; 17] = [ + "content-length", + "content-type", + "content-encoding", + "content-range", + "accept-ranges", + "etag", + "last-modified", + "cache-control", + "expires", + "vary", + "set-cookie", + "transfer-encoding", + "repr-digest", + "content-digest", + "digest", + "age", + "date", +]; + +/// Origin headers that describe the origin's bytes or one transfer of them, +/// never signed bytes. +const ORIGIN_REPRESENTATION_HEADERS: [&str; 11] = [ + "content-length", + "content-encoding", + "content-range", + "accept-ranges", + "etag", + "last-modified", + "repr-digest", + "content-digest", + "digest", + "content-md5", + "transfer-encoding", +]; + +/// Origin headers a kept signed representation carries besides the ones +/// core and the signer write. Core's per-request headers are written again +/// on each reader's copy, so none of them is kept. +const KEPT_ORIGIN_HEADERS: [&str; 6] = [ + "cache-control", + "expires", + "content-disposition", + "content-language", + "link", + "x-content-type-options", +]; + +/// Request headers that make an origin answer with less than the whole +/// representation. +const PRECONDITION_HEADERS: [HeaderName; 6] = [ + header::RANGE, + header::IF_RANGE, + header::IF_NONE_MATCH, + header::IF_MODIFIED_SINCE, + header::IF_MATCH, + header::IF_UNMODIFIED_SINCE, +]; + +/// The first bytes of a kept signed representation, naming its layout. +const KEPT_MAGIC: &[u8; 4] = b"TSM2"; + +/// How many kept signed representations one process holds in memory. Each +/// may be as large as a signer's limit, so few are held. +const KEPT_MEMORY_ENTRIES: usize = 4; + +/// What starts the opaque part of a signed representation's entity tag, +/// so a reader's precondition naming signed bytes is told from one naming +/// the origin's. +const SIGNED_TAG_PREFIX: &str = "sig-"; + +/// The `Cache-Control` an answer core meant to sign and could not goes out +/// with, so no cache holds unsigned bytes in place of signed ones for long. +const UNSIGNED_FALLBACK_CACHE_CONTROL: &str = "private, max-age=60"; + +/// `Sec-Fetch-Dest` values a browser sends for media. +const MEDIA_DESTINATIONS: [&str; 7] = [ + "image", "audio", "video", "track", "font", "object", "embed", +]; + +/// File extensions of media, for a request that names neither a media +/// destination nor a media type it accepts. +const MEDIA_EXTENSIONS: [&str; 33] = [ + "jpg", "jpeg", "png", "gif", "webp", "avif", "heic", "heif", "tif", "tiff", "svg", "bmp", + "ico", "jxl", "mp4", "m4v", "mov", "m4a", "mp3", "wav", "aac", "flac", "ogg", "oga", "ogv", + "opus", "webm", "avi", "mkv", "woff", "woff2", "ttf", "otf", +]; + +/// What a lookup that finds nothing kept answers, never stored. +const NOT_KEPT: &str = "no signed representation kept"; + +/// Whether `name` is a header core owns on a signed representation. +#[must_use] +pub fn is_reserved_header(name: &HeaderName) -> bool { + RESERVED_HEADERS.contains(&name.as_str()) +} + +/// Removes every header that would make the origin answer with less than +/// the whole representation, for the second, whole fetch of a claim. +pub(crate) fn strip_representation_preconditions(headers: &mut HeaderMap) { + for name in &PRECONDITION_HEADERS { + headers.remove(name); + } +} + +/// What the reader's request asked of the representation. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct ReaderPreconditions { + if_match: Option, + if_none_match: Option, + if_range: Option, + range: Option, + if_modified_since: Option, + if_unmodified_since: Option, +} + +impl ReaderPreconditions { + fn of(headers: &HeaderMap) -> Self { + Self { + if_match: joined(headers, &header::IF_MATCH), + if_none_match: joined(headers, &header::IF_NONE_MATCH), + if_range: joined(headers, &header::IF_RANGE), + range: joined(headers, &header::RANGE), + if_modified_since: joined(headers, &header::IF_MODIFIED_SINCE), + if_unmodified_since: joined(headers, &header::IF_UNMODIFIED_SINCE), + } + } + + /// Whether `If-Range` or `If-Match` names a signed representation's + /// entity tag, see [`SIGNED_TAG_PREFIX`]. + fn names_signed_bytes(&self) -> bool { + let if_range = self + .if_range + .as_deref() + .and_then(single_entity_tag) + .is_some_and(|tag| is_signed_tag(&tag)); + let if_match = self + .if_match + .as_deref() + .is_some_and(|value| match TagList::parse(value) { + TagList::Any => false, + TagList::Tags(tags) => tags.iter().any(is_signed_tag), + }); + if_range || if_match + } +} + +/// Whether `tag` is a strong entity tag core gave a signed representation. +fn is_signed_tag(tag: &EntityTag<'_>) -> bool { + !tag.weak + && tag + .opaque + .strip_prefix(SIGNED_TAG_PREFIX) + .is_some_and(|digest| { + digest.len() == 64 + && digest + .bytes() + .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + }) +} + +/// The strong entity tag of signed bytes whose SHA-256 is `digest`. +fn signed_entity_tag(digest: &[u8]) -> String { + format!("\"{SIGNED_TAG_PREFIX}{}\"", hex::encode(digest)) +} + +/// Whether a request plausibly asks for media: the destination a browser +/// names, else the first type it accepts, else the path's file extension. +fn request_looks_like_media(path: &str, headers: &HeaderMap) -> bool { + if let Some(destination) = headers + .get("sec-fetch-dest") + .and_then(|value| value.to_str().ok()) + { + let destination = destination.trim(); + return MEDIA_DESTINATIONS + .iter() + .any(|media| destination.eq_ignore_ascii_case(media)); + } + let accepted = header_text(headers, &header::ACCEPT) + .split(',') + .next() + .map(media_type_of) + .unwrap_or_default(); + if ["image/", "audio/", "video/", "font/"] + .iter() + .any(|prefix| accepted.starts_with(prefix)) + { + return true; + } + path.rsplit('/') + .next() + .and_then(|name| name.rsplit_once('.')) + .is_some_and(|(_, extension)| { + MEDIA_EXTENSIONS + .iter() + .any(|media| extension.eq_ignore_ascii_case(media)) + }) +} + +/// Seconds since the Unix epoch now, from the clock every runtime offers. +fn unix_seconds() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |since| since.as_secs()) +} + +/// An answer core meant to sign and could not, made safe to send unsigned: +/// without the origin's validators, which a later conditional request would +/// use to pin the unsigned bytes, and held only briefly and only by the +/// reader, so the next request can be signed. +fn unsigned_fallback(mut response: Response) -> Response { + crate::cache_policy::remove_owned_cache_headers(&mut response); + let headers = response.headers_mut(); + for name in ["etag", "last-modified", "expires"] { + headers.remove(name); + } + headers.insert( + header::CACHE_CONTROL, + HeaderValue::from_static(UNSIGNED_FALLBACK_CACHE_CONTROL), + ); + response +} + +/// Every line of header `name`, joined as one list. A value that is not +/// text stays present and matches nothing. +fn joined(headers: &HeaderMap, name: &HeaderName) -> Option { + let mut values = headers.get_all(name).iter().peekable(); + values.peek()?; + Some( + values + .map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned()) + .collect::>() + .join(", "), + ) +} + +/// What names one signed representation before the origin is asked: the +/// reader's address, the origin core asks and the address it asks there. +#[derive(Debug, Clone, Copy)] +pub(crate) struct VariantIdentity<'a> { + /// `scheme://host/path` the reader asked for, without its query. + pub reader_url: &'a str, + /// What tells this origin apart from another at one address. + pub origin_identity: &'a str, + /// The address core fetches, exactly as fetched. + pub fetched_url: &'a str, + /// The reader's actual query, without `?`, empty where absent. + pub reader_query: &'a str, +} + +/// Independent remaining browser and shared-cache lifetimes. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub(crate) struct OriginFreshness { + pub browser: Duration, + pub shared: Duration, +} + +/// What the origin's own headers said about its answer, read before core +/// changed or removed any of them, so a cache policy core writes never +/// stands in for the origin's. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct OriginFacts { + /// The origin marked the answer as one reader's, by a cookie, a + /// `Cache-Control` that forbids shared storage or a `Vary` on the + /// reader. + private: bool, + /// The origin forbade transforming the body. + no_transform: bool, + /// How long a signed representation of the answer may be kept. + keep_for: Option, + /// Freshness at receipt, before any response processing. + freshness: OriginFreshness, +} + +impl OriginFacts { + /// What `headers`, the origin's as it sent them, say. + pub(crate) fn of(headers: &HeaderMap) -> Self { + Self::of_at(headers, SystemTime::now()) + } + + /// Read against an explicit receipt time, also used by deterministic tests. + pub(crate) fn of_at(headers: &HeaderMap, received_at: SystemTime) -> Self { + let freshness = origin_freshness(headers, received_at); + Self { + private: headers.contains_key(header::SET_COOKIE) + || cache_control_forbids_shared_storage(headers) + || varies_by_reader(headers), + no_transform: cache_control_has(headers, "no-transform"), + keep_for: (!freshness.shared.is_zero()).then_some(freshness.shared), + freshness, + } + } + + /// The origin marked the answer as one reader's, by a cookie, a + /// `Cache-Control` that forbids shared storage or a `Vary` on the + /// reader. + pub(crate) fn is_private(&self) -> bool { + self.private + } + + /// The origin forbade transforming the body, by `no-transform`. + pub(crate) fn forbids_transform(&self) -> bool { + self.no_transform + } + + /// Freshness after processing, without borrowing the shared lifetime for + /// the browser or granting a new lifetime when origin dates are removed. + pub(crate) fn remaining_freshness(&self, elapsed: Duration) -> OriginFreshness { + OriginFreshness { + browser: self.freshness.browser.saturating_sub(elapsed), + shared: self.freshness.shared.saturating_sub(elapsed), + } + } +} + +/// Where the response handed to [`MediaClaim::deliver`] came from. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Answered { + /// The origin, to the request as core sent it, with what its own + /// headers said. + Origin(OriginFacts), + /// A kept signed representation [`MediaClaim::kept`] found. + Kept, +} + +/// What [`MediaClaim::deliver`] decided. +pub(crate) enum Delivery { + /// The response to send. + Send(Response), + /// The origin answered with a part, a `HEAD`, or a `412` or `416` a + /// reader's signed-length range or signed tag brings, for a + /// representation that may be signed. The caller asks the origin once + /// more for the whole representation, see + /// [`strip_representation_preconditions`], and hands the answer to + /// [`MediaClaim::deliver_whole`], with this response to send where that + /// fails. + FetchWhole(Response), + /// The origin answered with a source larger than the signer's bounded + /// limit that the large lane admits, pinned to its version. The caller + /// hands it to [`MediaClaim::deliver_large`] with how the origin is + /// asked again. + Large(LargeAnswer), +} + +/// A request the services' [`MediaSigner`] may sign the answer to. +pub(crate) struct MediaClaim { + signer: Arc, + reader_method: Method, + preconditions: ReaderPreconditions, + spec: CacheSpec, + /// The answer may be kept for, and a kept one served to, other + /// readers: the request carries nothing that could make the origin's + /// answer this reader's own. + shared: bool, + /// The request lets a kept signed representation answer it: it is + /// shared, does not ask for revalidation, and plausibly asks for media. + reads_kept: bool, + /// The request plausibly asks for media. + looks_like_media: bool, + /// The reader's `If-Range` or `If-Match` names a signed + /// representation, which the origin cannot judge. + names_signed_bytes: bool, + /// The answer is the platform image optimizer's, see + /// [`answered_by_optimizer`](Self::answered_by_optimizer). + optimizer_output: bool, + /// The claim answered the reader's preconditions against the + /// representation it delivered, see + /// [`answer_unsigned`](Self::answer_unsigned). + conditions_answered: AtomicBool, + /// The module state of a request the signer reads for its own + /// preparation, see [`MediaSigner::observes_snapshot`]. + snapshot: Option, +} + +impl MediaClaim { + /// The claim the request's services' signer makes on a request, or + /// `None` where no signer is registered, the method is neither `GET` + /// nor `HEAD`, the request carries `Authorization`, or the signer leaves + /// `path` alone. + /// + /// `shared` says whether nothing the request carries beyond these + /// headers could make the origin's answer this reader's own, being a + /// cookie sent to the origin, a module's request state or a + /// personalized delivery. Only a shared request reads or writes a kept + /// signed representation. + pub(crate) fn of_request( + settings: &Settings, + services: &RuntimeServices, + method: &Method, + path: &str, + headers: &HeaderMap, + shared: bool, + ) -> Option { + let signer = services.media_signer()?; + let claimed = (*method == Method::GET || *method == Method::HEAD) + && !headers.contains_key(header::AUTHORIZATION) + && signer.handles_path(path); + if !claimed { + return None; + } + let preconditions = ReaderPreconditions::of(headers); + let looks_like_media = request_looks_like_media(path, headers); + let names_signed_bytes = preconditions.names_signed_bytes(); + Some(Self { + signer: Arc::clone(signer), + reader_method: method.clone(), + preconditions, + spec: CacheSpec { + name: ASSETS_CACHE, + lifetimes: settings + .cache + .declared + .get(ASSETS_CACHE) + .map_or(ASSET_DEFAULT_LIFETIMES, |config| { + config.lifetimes(ASSET_DEFAULT_LIFETIMES) + }), + memory_entries: KEPT_MEMORY_ENTRIES, + }, + shared, + reads_kept: shared + && looks_like_media + && !signer.lookup_only() + && !crate::publisher::request_asks_for_revalidation(headers), + looks_like_media, + names_signed_bytes, + optimizer_output: false, + conditions_answered: AtomicBool::new(false), + snapshot: None, + }) + } + + /// The same claim on a request whose module hooks left `state`: where + /// the signer reads it for its own preparation, nothing is signed, + /// looked up or kept, and the origin's answer goes as it came with the + /// signer's snapshot headers, see [`MediaSigner::observes_snapshot`]. + pub(crate) fn observing(mut self, state: &IntegrationRequestState) -> Self { + if self.signer.observes_snapshot(state) { + self.reads_kept = false; + self.snapshot = Some(state.clone()); + } + self + } + + /// The origin's answer to a snapshot request, as it came, with what its + /// own headers said written by the signer. + fn snapshot_answer( + &self, + state: &IntegrationRequestState, + identity: &VariantIdentity<'_>, + facts: &OriginFacts, + mut response: Response, + ) -> Response { + let headers = response.headers(); + let encoding = header_text(headers, &header::CONTENT_ENCODING).trim(); + let snapshot = SnapshotFacts { + status: response.status(), + private: facts.private || headers.contains_key(header::SET_COOKIE), + no_transform: facts.no_transform, + manifest_link: carries_manifest_link(headers), + content_encoding: if encoding.eq_ignore_ascii_case("identity") { + String::new() + } else { + encoding.to_ascii_lowercase() + }, + origin_host: url::Url::parse(identity.fetched_url) + .ok() + .and_then(|url| url.host_str().map(str::to_ascii_lowercase)) + .unwrap_or_default(), + origin_identity_sha256: hex::encode(Sha256::digest( + identity.origin_identity.as_bytes(), + )), + source_reference: source_reference(identity.fetched_url).map(str::to_owned), + }; + for (name, value) in self.signer.snapshot_headers(state, &snapshot) { + if !is_reserved_header(&name) { + response.headers_mut().insert(name, value); + } + } + // The snapshot's own transport is internal: no cache may keep it + // for a reader. The origin's privacy was read above, before this + // header replaced its own, and travels in the signer's facts. + response.headers_mut().insert( + header::CACHE_CONTROL, + HeaderValue::from_static("private, no-store"), + ); + response + } + + /// The same claim on a request the platform's image optimizer answers. + /// + /// The optimizer reads the origin itself, so core never holds its + /// input. Its answer is signed only where it carries a C2PA manifest + /// store, the optimizer's own record of the input it read and what it + /// did, and the signer must refuse unless that manifest is present and + /// intact, see [`MediaJob::optimizer_output`]. Its answer depends on + /// the transformation and the reader's `Accept`, neither of which names + /// a kept representation, so nothing is read from or written to the + /// kept representations, and only a whole `200` to a `GET` is signed: + /// a part or `HEAD` is never asked for again past the optimizer. + pub(crate) fn answered_by_optimizer(mut self) -> Self { + self.shared = false; + self.reads_kept = false; + self.optimizer_output = true; + self + } + + /// Whether the request plausibly asks for media, by the destination + /// the browser names, the type it accepts first, or the file extension. + /// The origin is then asked for an identity-coded body, the only coding + /// signed. + pub(crate) fn looks_like_media(&self) -> bool { + self.looks_like_media + } + + /// Whether the reader's `If-Range` or `If-Match` names a signed + /// representation. The origin cannot judge such a precondition, so the + /// caller asks it for the whole representation, see + /// [`strip_representation_preconditions`], and core answers the + /// reader's range and preconditions against the signed bytes. + pub(crate) fn names_signed_bytes(&self) -> bool { + self.names_signed_bytes + } + + /// Whether the answer is the platform image optimizer's, see + /// [`answered_by_optimizer`](Self::answered_by_optimizer). + pub(crate) fn signs_optimizer_output(&self) -> bool { + self.optimizer_output + } + + /// The signed representation kept for `identity` under the signer's + /// current namespace, as an origin answer to hand to + /// [`deliver`](Self::deliver) with [`Answered::Kept`], or `None`. Only + /// a shared request that plausibly asks for media and asks for no + /// revalidation is looked up, so the signer's namespace is not resolved + /// for any other. + pub(crate) async fn kept( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + ) -> Option> { + if !self.reads_kept || !services.loading_cache_for(ASSETS_CACHE).keeps_values() { + return None; + } + let namespace = match self.signer.namespace(services).await { + Ok(namespace) => namespace, + Err(failure) => { + log::debug!( + "media signing: nothing kept looked up signer={} reason={failure}", + self.signer.signer_id() + ); + return None; + } + }; + let key = self.store_key(services, &namespace, identity); + let bytes = services + .load_cached( + &self.spec, + &key, + Box::new(|| Box::pin(async { Err(LoadError::new(NOT_KEPT)) })), + ) + .await + .ok()?; + let kept = decode_kept(&bytes, unix_seconds()); + if kept.is_none() { + log::warn!( + "media signing: kept representation unreadable or altered signer={}", + self.signer.signer_id() + ); + services + .loading_cache_for(ASSETS_CACHE) + .remove(&services.clone().without_connection(), &self.spec, &key) + .await; + } + kept + } + + /// Answers the reader from `response`: a kept signed representation, or + /// the origin's answer, signed where the signer signs it. + pub(crate) async fn deliver( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + answered: Answered, + response: Response, + ) -> Delivery { + let facts = match answered { + Answered::Kept => return Delivery::Send(self.respond_kept(response)), + Answered::Origin(facts) => facts, + }; + if let Some(state) = &self.snapshot { + return Delivery::Send(self.snapshot_answer(state, identity, &facts, response)); + } + if self.optimizer_output { + return Delivery::Send( + self.deliver_optimized(services, identity, &facts, response) + .await, + ); + } + let signer_id = self.signer.signer_id(); + let status = response.status(); + let limit = to_u64(self.signer.max_source_bytes()); + match status { + StatusCode::OK if self.reader_method != Method::HEAD => { + if let Some(reason) = self.unsignable_reason(response.headers(), &facts) { + log::debug!( + "media signing: outcome=unsigned signer={signer_id} reason={reason}" + ); + return Delivery::Send(response); + } + let total = declared_length(response.headers()); + if total.is_some_and(|total| total > limit) { + return self.large_or_unsigned(services, response, total, &facts); + } + let (parts, body) = response.into_parts(); + let source = match self.capture_within_limit(&parts.headers, body).await { + Ok(source) => source, + Err(body) => return Delivery::Send(Response::from_parts(parts, body)), + }; + if is_fragmented_stream_part(&source) { + log::info!( + "media signing: outcome=unsigned signer={signer_id} reason=fragmented_stream_part" + ); + return Delivery::Send(Response::from_parts(parts, EdgeBody::Once(source))); + } + match self.sign(services, identity, &parts.headers, &source).await { + Ok(signed) => Delivery::Send( + self.respond_signed(services, identity, parts, signed, facts.keep_for) + .await, + ), + Err(()) => Delivery::Send(unsigned_fallback(Response::from_parts( + parts, + EdgeBody::Once(source), + ))), + } + } + StatusCode::OK | StatusCode::PARTIAL_CONTENT => { + if let Some(reason) = self.unsignable_reason(response.headers(), &facts) { + log::debug!( + "media signing: outcome=unsigned signer={signer_id} reason={reason}" + ); + return Delivery::Send(response); + } + let total = if status == StatusCode::PARTIAL_CONTENT { + content_range_total(response.headers()) + } else { + declared_length(response.headers()) + }; + let too_large = if status == StatusCode::PARTIAL_CONTENT { + total.is_none_or(|total| total > limit) + } else { + total.is_some_and(|total| total > limit) + }; + if too_large { + return self.large_or_unsigned(services, response, total, &facts); + } + // A part or a `HEAD` is signed only where the signed whole + // can be kept, so the reader's next range is answered from + // it and the origin is not read whole for every range. + if !self.retains(services, &facts) { + log::info!( + "media signing: outcome=unsigned signer={signer_id} reason=part_not_kept" + ); + return Delivery::Send(response); + } + Delivery::FetchWhole(response) + } + StatusCode::PRECONDITION_FAILED | StatusCode::RANGE_NOT_SATISFIABLE + if self.preconditions.range.is_some() || self.preconditions.if_match.is_some() => + { + // The reader's range or `If-Match` may describe the signed + // representation, which is longer than the origin's and has + // a tag of its own, so the origin's refusal is not the + // answer until the signed bytes are asked. + let too_large = + content_range_total(response.headers()).is_some_and(|total| total > limit); + if facts.private || too_large { + return Delivery::Send(response); + } + Delivery::FetchWhole(response) + } + StatusCode::NOT_MODIFIED => Delivery::Send( + self.decorate_not_modified(services, identity, &facts, response) + .await, + ), + _ => Delivery::Send(response), + } + } + + /// Answers the reader from the platform image optimizer's answer: a + /// whole `200` to a `GET` that carries a C2PA manifest store is signed, + /// and every other answer goes as the optimizer made it. + async fn deliver_optimized( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + facts: &OriginFacts, + response: Response, + ) -> Response { + let signer_id = self.signer.signer_id(); + if response.status() != StatusCode::OK || self.reader_method != Method::GET { + return response; + } + if let Some(reason) = self.unsignable_reason(response.headers(), facts) { + log::debug!("media signing: outcome=unsigned signer={signer_id} reason={reason}"); + return response; + } + let (parts, body) = response.into_parts(); + let source = match self.capture_within_limit(&parts.headers, body).await { + Ok(source) => source, + Err(body) => return Response::from_parts(parts, body), + }; + // The optimizer writes a manifest only where its input carried one. + // Without it nothing records what the optimizer read or did, so the + // answer is not signed. + if !carries_c2pa_manifest_store(&source) { + log::info!( + "media signing: outcome=unsigned signer={signer_id} reason=image_optimizer_output_without_c2pa" + ); + return Response::from_parts(parts, EdgeBody::Once(source)); + } + // The optimizer may choose its output format from the reader's + // `Accept`, and it removes the origin's `Vary`, so a cache past core + // is told the signed bytes depend on it. + let mut parts = parts; + let varies_by_accept = parts + .headers + .get_all(header::VARY) + .iter() + .filter_map(|value| value.to_str().ok()) + .flat_map(|value| value.split(',')) + .any(|name| name.trim() == "*" || name.trim().eq_ignore_ascii_case("accept")); + if !varies_by_accept { + parts + .headers + .append(header::VARY, HeaderValue::from_static("Accept")); + } + match self.sign(services, identity, &parts.headers, &source).await { + Ok(signed) => { + self.respond_signed(services, identity, parts, signed, None) + .await + } + Err(()) => unsigned_fallback(Response::from_parts(parts, EdgeBody::Once(source))), + } + } + + /// Answers the reader from the origin's answer to the whole, + /// unconditional request [`Delivery::FetchWhole`] asked for, or sends + /// `fallback` where it cannot be signed. + /// + /// `whole` is the origin's answer as it came, whose own headers decide + /// what is signed and how long it is kept. The signed response is + /// written on `fallback`'s headers, which carry what core wrote for + /// this reader. + pub(crate) async fn deliver_whole( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + whole: Response, + fallback: Response, + ) -> Response { + let signer_id = self.signer.signer_id(); + // The answer sent where the whole cannot be signed already declares + // a manifest of the origin's own, which core never competes with. + if carries_manifest_link(fallback.headers()) { + log::info!( + "media signing: outcome=unsigned signer={signer_id} reason=origin_manifest_link" + ); + return fallback; + } + let facts = OriginFacts::of(whole.headers()); + if whole.status() != StatusCode::OK { + log::info!( + "media signing: outcome=unsigned signer={signer_id} reason=whole_fetch_status_{}", + whole.status().as_u16() + ); + return unsigned_fallback(fallback); + } + if let Some(reason) = self.unsignable_reason(whole.headers(), &facts) { + log::info!("media signing: outcome=unsigned signer={signer_id} reason={reason}"); + return fallback; + } + let (whole_parts, whole_body) = whole.into_parts(); + let Ok(source) = self + .capture_within_limit(&whole_parts.headers, whole_body) + .await + else { + return fallback; + }; + // A part of a fragmented stream goes as the origin answered it. + if is_fragmented_stream_part(&source) { + log::info!( + "media signing: outcome=unsigned signer={signer_id} reason=fragmented_stream_part" + ); + return fallback; + } + match self + .sign(services, identity, &whole_parts.headers, &source) + .await + { + Ok(signed) => { + let (parts, _) = fallback.into_parts(); + self.respond_signed(services, identity, parts, signed, facts.keep_for) + .await + } + Err(()) => unsigned_fallback(fallback), + } + } + + /// Whether a signed whole made for this request can be kept: the request + /// is shared, the origin gave its answer a shared freshness, and the + /// deployment's asset cache keeps values. + fn retains(&self, services: &RuntimeServices, facts: &OriginFacts) -> bool { + self.shared + && facts.keep_for.is_some() + && services.loading_cache_for(ASSETS_CACHE).keeps_values() + } + + /// The whole body of a signable `200`, or the body as it stands where + /// it is larger than the signer's limit or could not be read, so the + /// reader still receives every byte the origin sent. + async fn capture_within_limit( + &self, + headers: &HeaderMap, + body: EdgeBody, + ) -> Result { + let signer_id = self.signer.signer_id(); + let limit = self.signer.max_source_bytes(); + if declared_length(headers).is_some_and(|length| length > to_u64(limit)) { + log::info!("media signing: outcome=unsigned signer={signer_id} reason=exceeds_limit"); + return Err(body); + } + match capture(body, limit).await { + Captured::Whole(source) => Ok(source), + Captured::Partial(body) => { + log::info!( + "media signing: outcome=unsigned signer={signer_id} reason=exceeds_limit_or_unreadable" + ); + Err(body) + } + } + } + + /// Why a response is not handed to the signer, or `None` when it is. + /// `facts` are the origin's own, read before core wrote its cache + /// policy on `headers`. + fn unsignable_reason(&self, headers: &HeaderMap, facts: &OriginFacts) -> Option<&'static str> { + if facts.private || headers.contains_key(header::SET_COOKIE) { + return Some("origin_private"); + } + // Provenance the origin already links to describes these bytes, so + // they are neither signed again nor given a competing manifest. + if carries_manifest_link(headers) { + return Some("origin_manifest_link"); + } + if facts.no_transform { + return Some("no_transform"); + } + let encoding = header_text(headers, &header::CONTENT_ENCODING).trim(); + if !encoding.is_empty() && !encoding.eq_ignore_ascii_case("identity") { + return Some("content_encoded"); + } + let media_type = media_type_of(header_text(headers, &header::CONTENT_TYPE)); + if media_type.is_empty() || !self.signer.handles_media_type(&media_type) { + return Some("media_type"); + } + None + } + + /// Asks the signer for the signed representation of `source`, logging + /// the outcome with no address, since a reader's or origin's address + /// may carry a credential. + async fn sign( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + headers: &HeaderMap, + source: &Bytes, + ) -> Result { + let signer_id = self.signer.signer_id(); + let source_sha256 = hex::encode(Sha256::digest(source)); + let origin_host = url::Url::parse(identity.fetched_url) + .ok() + .and_then(|url| url.host_str().map(str::to_ascii_lowercase)); + let job = MediaJob { + reader_url: identity.reader_url, + reader_query: identity.reader_query, + content_type: header_text(headers, &header::CONTENT_TYPE), + source, + source_sha256: &source_sha256, + // No address serves the optimizer's answer. + source_reference: if self.optimizer_output { + None + } else { + source_reference(identity.fetched_url) + }, + source_origin_host: origin_host.as_deref(), + optimizer_output: self.optimizer_output, + }; + let checked = match self.signer.sign(services, &job).await { + Ok(signed) if signed.bytes.is_empty() => Err(MediaSigningFailure { + reason: "empty_signed_body", + }), + Ok(signed) + if signed + .headers + .iter() + .any(|(name, _)| is_reserved_header(name)) => + { + Err(MediaSigningFailure { + reason: "reserved_header", + }) + } + Ok(signed) if signed.namespace.is_empty() => Err(MediaSigningFailure { + reason: "no_namespace", + }), + other => other, + }; + match checked { + Ok(signed) => { + log::info!( + "media signing: outcome=signed signer={signer_id} source_bytes={} signed_bytes={}", + source.len(), + signed.bytes.len() + ); + Ok(signed) + } + Err(failure) => { + log::warn!("media signing: outcome=unsigned signer={signer_id} reason={failure}"); + Err(()) + } + } + } + + /// The signed representation, kept for `keep_for` where this request is + /// shared, and answered against the reader's request. Ranges are offered + /// only on a representation that was kept, so a reader's later range is + /// answered from it and never from a part of the origin's own bytes. + async fn respond_signed( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + mut parts: http::response::Parts, + signed: SignedMedia, + keep_for: Option, + ) -> Response { + let origin_age = digits(header_text(&parts.headers, &header::AGE).trim()).unwrap_or(0); + for name in ORIGIN_REPRESENTATION_HEADERS { + parts.headers.remove(name); + } + let digest = Sha256::digest(&signed.bytes); + let etag = signed_entity_tag(&digest); + let repr_digest = format!( + "sha-256=:{}:", + base64::engine::general_purpose::STANDARD.encode(digest) + ); + let mut representation = HeaderMap::new(); + representation.insert(header::CONTENT_TYPE, signed.content_type); + representation.insert( + header::ETAG, + HeaderValue::from_str(&etag).expect("should build a hex entity tag"), + ); + representation.insert( + HeaderName::from_static("repr-digest"), + HeaderValue::from_str(&repr_digest).expect("should build a base64 digest"), + ); + for name in KEPT_ORIGIN_HEADERS { + for value in parts.headers.get_all(name) { + representation.append(HeaderName::from_static(name), value.clone()); + } + } + for (name, value) in signed.headers { + if name == header::LINK { + representation.append(name, value); + } else { + representation.insert(name, value); + } + } + let kept = match keep_for.filter(|_| self.shared && !self.signer.lookup_only()) { + Some(lifetime) => { + let value = encode_kept(&KeptRecord { + kept_at: unix_seconds(), + age: origin_age, + headers: &representation, + body: &signed.bytes, + }); + self.keep(services, identity, &signed.namespace, value, lifetime) + .await + } + None => false, + }; + for name in representation.keys() { + parts.headers.remove(name); + } + for (name, value) in &representation { + parts.headers.append(name.clone(), value.clone()); + } + self.answer(parts, signed.bytes, kept) + } + + /// A kept signed representation, answered against the reader's + /// request. + fn respond_kept(&self, response: Response) -> Response { + let (parts, body) = response.into_parts(); + let bytes = body.into_bytes().unwrap_or_default(); + self.answer(parts, bytes, true) + } + + /// Answers the reader's preconditions and range against the signed + /// `bytes`, whose `ETag` is in `parts`. + fn answer( + &self, + parts: http::response::Parts, + bytes: Bytes, + ranges_offered: bool, + ) -> Response { + let length = to_u64(bytes.len()); + let etag = parts + .headers + .get(header::ETAG) + .and_then(|value| value.to_str().ok()) + .map(str::to_owned); + let outcome = evaluate( + &self.reader_method, + &self.preconditions, + etag.as_deref(), + None, + length, + ); + self.conditions_answered.store(true, Ordering::Relaxed); + self.apply(parts, bytes, outcome, ranges_offered) + } + + /// `response`, a whole `200` this claim delivered, answered against the + /// reader's preconditions where the claim has not already answered them, + /// which it has for every signed representation it delivered. For a + /// request whose preconditions were not sent to the origin, so the + /// origin's whole answer is checked against them here, by the + /// representation's own `ETag` and `Last-Modified`. A range is not + /// applied, so a passing answer goes whole, as RFC 9110 allows. + pub(crate) fn answer_unsigned(&self, response: Response) -> Response { + if self.conditions_answered.load(Ordering::Relaxed) { + return response; + } + answer_whole_with(&self.reader_method, &self.preconditions, response) + } +} + +/// `response`, a whole `200`, answered against the preconditions of a +/// reader's request carrying `reader_headers`, by the representation's own +/// `ETag` and `Last-Modified`, see [`MediaClaim::answer_unsigned`]: `412` +/// or `304` with no body, or the response as it is. A range is not applied. +pub(crate) fn answer_whole( + method: &Method, + reader_headers: &HeaderMap, + response: Response, +) -> Response { + answer_whole_with(method, &ReaderPreconditions::of(reader_headers), response) +} + +fn answer_whole_with( + method: &Method, + preconditions: &ReaderPreconditions, + response: Response, +) -> Response { + if response.status() != StatusCode::OK { + return response; + } + let headers = response.headers(); + let etag = headers + .get(header::ETAG) + .and_then(|value| value.to_str().ok()) + .map(str::to_owned); + let last_modified = headers + .get(header::LAST_MODIFIED) + .and_then(|value| value.to_str().ok()) + .and_then(|value| httpdate::parse_http_date(value.trim()).ok()); + let whole = ReaderPreconditions { + range: None, + if_range: None, + ..preconditions.clone() + }; + let outcome = evaluate(method, &whole, etag.as_deref(), last_modified, 0); + let (mut parts, body) = response.into_parts(); + match outcome { + Outcome::NotModified => { + parts.status = StatusCode::NOT_MODIFIED; + for name in ["content-length", "content-range", "transfer-encoding"] { + parts.headers.remove(name); + } + Response::from_parts(parts, EdgeBody::empty()) + } + Outcome::PreconditionFailed => { + parts.status = StatusCode::PRECONDITION_FAILED; + for name in [ + "content-type", + "content-range", + "transfer-encoding", + "content-encoding", + ] { + parts.headers.remove(name); + } + parts + .headers + .insert(header::CONTENT_LENGTH, HeaderValue::from(0_u64)); + Response::from_parts(parts, EdgeBody::empty()) + } + _ => Response::from_parts(parts, body), + } +} + +/// Removes the headers that describe the origin's bytes or one transfer of +/// them, for a representation core made of them. +pub(crate) fn remove_origin_representation_headers(headers: &mut HeaderMap) { + for name in ORIGIN_REPRESENTATION_HEADERS { + headers.remove(name); + } +} + +impl MediaClaim { + /// Keeps a signed representation for `identity` under `namespace`, the + /// one it was signed under, so a later request is answered before the + /// origin is asked. Whether it was kept. + async fn keep( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + namespace: &str, + value: Bytes, + lifetime: Duration, + ) -> bool { + if !services.loading_cache_for(ASSETS_CACHE).keeps_values() { + return false; + } + let key = self.store_key(services, namespace, identity); + let stored = services + .load_cached( + &self.spec, + &key, + Box::new(move || { + Box::pin(async move { Ok(LoadedValue::new(value).with_lifetime(lifetime)) }) + }), + ) + .await; + match stored { + Ok(_) => true, + Err(error) => { + log::debug!( + "media signing: representation not kept signer={} error={error}", + self.signer.signer_id() + ); + false + } + } + } + + /// The key a signed representation for `identity` is kept under, bound + /// to `namespace`. + fn store_key( + &self, + services: &RuntimeServices, + namespace: &str, + identity: &VariantIdentity<'_>, + ) -> StoreKey { + let mut digest = Sha256::new(); + for part in [ + self.signer.signer_id(), + namespace, + identity.reader_url, + identity.origin_identity, + identity.fetched_url, + ] { + digest.update(part.as_bytes()); + digest.update([0]); + } + StoreKey::new( + services.server_key_prefix(), + ASSETS_CACHE, + format!("media-{}", hex::encode(digest.finalize())), + ) + } + + /// Writes `outcome` on the response: its status, its framing headers and + /// the part of `bytes` it carries. + fn apply( + &self, + mut parts: http::response::Parts, + bytes: Bytes, + outcome: Outcome, + ranges_offered: bool, + ) -> Response { + let length = to_u64(bytes.len()); + let headers = &mut parts.headers; + for name in [ + "content-range", + "content-length", + "transfer-encoding", + "content-encoding", + ] { + headers.remove(name); + } + headers.insert( + header::ACCEPT_RANGES, + HeaderValue::from_static(if ranges_offered { "bytes" } else { "none" }), + ); + let carries_body = self.reader_method != Method::HEAD; + let body = match outcome { + Outcome::Full => { + parts.status = StatusCode::OK; + headers.insert(header::CONTENT_LENGTH, HeaderValue::from(length)); + if carries_body { + EdgeBody::Once(bytes) + } else { + EdgeBody::empty() + } + } + Outcome::NotModified => { + parts.status = StatusCode::NOT_MODIFIED; + EdgeBody::empty() + } + Outcome::PreconditionFailed => { + parts.status = StatusCode::PRECONDITION_FAILED; + headers.remove(header::CONTENT_TYPE); + headers.insert(header::CONTENT_LENGTH, HeaderValue::from(0_u64)); + EdgeBody::empty() + } + Outcome::Partial { first, last } => { + parts.status = StatusCode::PARTIAL_CONTENT; + headers.insert( + header::CONTENT_RANGE, + HeaderValue::from_str(&format!("bytes {first}-{last}/{length}")) + .expect("should build a content range"), + ); + headers.insert(header::CONTENT_LENGTH, HeaderValue::from(last - first + 1)); + let from = usize::try_from(first) + .unwrap_or(bytes.len()) + .min(bytes.len()); + let to = usize::try_from(last) + .map_or(bytes.len(), |last| last.saturating_add(1)) + .min(bytes.len()); + EdgeBody::Once(bytes.slice(from..to.max(from))) + } + Outcome::Unsatisfiable => { + parts.status = StatusCode::RANGE_NOT_SATISFIABLE; + headers.insert( + header::CONTENT_RANGE, + HeaderValue::from_str(&format!("bytes */{length}")) + .expect("should build a content range"), + ); + headers.remove(header::CONTENT_TYPE); + headers.insert(header::CONTENT_LENGTH, HeaderValue::from(0_u64)); + EdgeBody::empty() + } + }; + Response::from_parts(parts, body) + } +} + +/// Origin freshness at receipt. Revalidation-only answers and reader-varying +/// responses authorize no reusable copy. Shared freshness falls back to +/// `max-age` only when `s-maxage` is absent, never when it is malformed. +fn origin_freshness(headers: &HeaderMap, received_at: SystemTime) -> OriginFreshness { + if ["no-store", "no-cache", "private"] + .iter() + .any(|directive| cache_control_has(headers, directive)) + || headers + .get_all(header::VARY) + .iter() + .flat_map(|value| value.to_str().unwrap_or("*").split(',')) + .map(str::trim) + .any(|name| !name.is_empty() && !name.eq_ignore_ascii_case("accept-encoding")) + { + return OriginFreshness::default(); + } + let age = match headers.get(header::AGE) { + Some(value) => match value.to_str().ok().and_then(|value| digits(value.trim())) { + Some(seconds) => Duration::from_secs(seconds), + None => return OriginFreshness::default(), + }, + None => Duration::ZERO, + }; + let apparent_age = match headers.get(header::DATE) { + Some(value) => match value + .to_str() + .ok() + .and_then(|value| httpdate::parse_http_date(value.trim()).ok()) + { + Some(date) => received_at + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .saturating_sub( + date.duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default(), + ), + None => return OriginFreshness::default(), + }, + None => Duration::ZERO, + }; + let current_age = age.max(apparent_age); + let browser = cache_control_seconds(headers, "max-age"); + let shared = match cache_control_seconds(headers, "s-maxage") { + Ok(None) => browser, + value => value, + }; + let remaining = |seconds: Result, ()>| { + Duration::from_secs(seconds.ok().flatten().unwrap_or(0)).saturating_sub(current_age) + }; + OriginFreshness { + browser: remaining(browser), + shared: remaining(shared), + } +} + +/// An absent directive permits fallback; an invalid or repeated one does not. +fn cache_control_seconds(headers: &HeaderMap, directive: &str) -> Result, ()> { + let mut seconds = None; + for line in headers.get_all(header::CACHE_CONTROL) { + for part in line.to_str().map_err(|_| ())?.split(',') { + let (name, value) = part.split_once('=').unwrap_or((part, "")); + if !name.trim().eq_ignore_ascii_case(directive) { + continue; + } + if seconds.is_some() { + return Err(()); + } + let value = value.trim(); + let value = if let Some(quoted) = value.strip_prefix('"') { + quoted.strip_suffix('"').ok_or(())? + } else { + value + }; + seconds = Some(digits(value).ok_or(())?); + } + } + Ok(seconds) +} + +/// What a kept signed representation holds besides its key. +struct KeptRecord<'a> { + /// When it was kept, in seconds since the Unix epoch. + kept_at: u64, + /// The `Age` the origin's answer carried when it was kept. + age: u64, + headers: &'a HeaderMap, + body: &'a Bytes, +} + +/// A kept signed representation, as written: [`KEPT_MAGIC`], when it was +/// kept and the age it had then, the header count, each header's name and +/// value with their lengths, then the body. +fn encode_kept(record: &KeptRecord<'_>) -> Bytes { + let mut out = BytesMut::with_capacity(record.body.len() + 512); + out.extend_from_slice(KEPT_MAGIC); + out.extend_from_slice(&record.kept_at.to_be_bytes()); + out.extend_from_slice(&record.age.to_be_bytes()); + let count = u32::try_from(record.headers.len()).unwrap_or(u32::MAX); + out.extend_from_slice(&count.to_be_bytes()); + for (name, value) in record + .headers + .iter() + .take(usize::try_from(count).unwrap_or(usize::MAX)) + { + let name = name.as_str().as_bytes(); + let value = value.as_bytes(); + out.extend_from_slice(&u32::try_from(name.len()).unwrap_or(u32::MAX).to_be_bytes()); + out.extend_from_slice(name); + out.extend_from_slice(&u32::try_from(value.len()).unwrap_or(u32::MAX).to_be_bytes()); + out.extend_from_slice(value); + } + out.extend_from_slice(record.body); + out.freeze() +} + +/// A kept signed representation as a `200` answer at `now`, with the `Age` +/// it has reached, or `None` where the bytes are not one [`encode_kept`] +/// wrote or the body is not the one its entity tag names. +fn decode_kept(bytes: &Bytes, now: u64) -> Option> { + let mut reader = KeptReader { + rest: bytes.strip_prefix(KEPT_MAGIC.as_slice())?, + }; + let kept_at = reader.number()?; + let age = reader.number()?; + let count = reader.length()?; + let mut headers = HeaderMap::new(); + for _ in 0..count { + let name_length = reader.length()?; + let name = HeaderName::from_bytes(reader.take(name_length)?).ok()?; + let value_length = reader.length()?; + let value = HeaderValue::from_bytes(reader.take(value_length)?).ok()?; + headers.append(name, value); + } + let body = bytes.slice(bytes.len() - reader.rest.len()..); + let expected = signed_entity_tag(&Sha256::digest(&body)); + if header_text(&headers, &header::ETAG) != expected { + return None; + } + headers.insert( + header::AGE, + HeaderValue::from(age.saturating_add(now.saturating_sub(kept_at))), + ); + let mut response = Response::new(EdgeBody::Once(body)); + *response.headers_mut() = headers; + Some(response) +} + +/// Reads a kept representation's fields in order. +struct KeptReader<'a> { + rest: &'a [u8], +} + +impl<'a> KeptReader<'a> { + fn take(&mut self, length: usize) -> Option<&'a [u8]> { + let taken = self.rest.get(..length)?; + self.rest = self.rest.get(length..)?; + Some(taken) + } + + fn length(&mut self) -> Option { + let bytes: [u8; 4] = self.take(4)?.try_into().ok()?; + usize::try_from(u32::from_be_bytes(bytes)).ok() + } + + fn number(&mut self) -> Option { + let bytes: [u8; 8] = self.take(8)?.try_into().ok()?; + Some(u64::from_be_bytes(bytes)) + } +} + +/// The fetched address a signer may name as the source, being the address +/// exactly as fetched when it carries no user information, query or +/// fragment, which could hold a credential or a reader's data. +fn source_reference(fetched_url: &str) -> Option<&str> { + let parsed = url::Url::parse(fetched_url).ok()?; + let plain = parsed.username().is_empty() + && parsed.password().is_none() + && parsed.query().is_none() + && parsed.fragment().is_none(); + plain.then_some(fetched_url) +} + +/// Whether `Vary` names `*`, `Cookie` or `Authorization`, so the origin's +/// answer may differ between readers. +fn varies_by_reader(headers: &HeaderMap) -> bool { + headers + .get_all(header::VARY) + .iter() + .filter_map(|value| value.to_str().ok()) + .flat_map(|value| value.split(',')) + .map(str::trim) + .any(|name| { + name == "*" + || name.eq_ignore_ascii_case("cookie") + || name.eq_ignore_ascii_case("authorization") + }) +} + +/// Whether the origin's own `Link` already names a C2PA manifest, so the +/// representation carries provenance core must keep as it came rather +/// than sign again or compete with. +pub(crate) fn carries_manifest_link(headers: &HeaderMap) -> bool { + headers + .get_all(header::LINK) + .iter() + .filter_map(|value| value.to_str().ok()) + .flat_map(|value| value.split(';')) + .filter_map(|parameter| { + let (name, relations) = parameter.trim().split_once('=')?; + name.trim().eq_ignore_ascii_case("rel").then_some(relations) + }) + .flat_map(|relations| { + relations.split(|character: char| { + character.is_ascii_whitespace() || character == '"' || character == ',' + }) + }) + .any(|relation| relation.eq_ignore_ascii_case("c2pa-manifest")) +} + +/// The JUMBF description box of a C2PA manifest store: the `jumd` box type +/// followed by the C2PA manifest store UUID, +/// `63327061-0011-0010-8000-00AA00389B71`. Every embedding of a store, a +/// JPEG `APP11` segment, a PNG `caBX` chunk, a WebP `C2PA` chunk, an ISO +/// BMFF `uuid` box or a JPEG XL `jumb` box, carries these bytes +/// contiguously at the head of the store. +const C2PA_STORE_DESCRIPTION: [u8; 20] = [ + b'j', b'u', b'm', b'd', 0x63, 0x32, 0x70, 0x61, 0x00, 0x11, 0x00, 0x10, 0x80, 0x00, 0x00, 0xAA, + 0x00, 0x38, 0x9B, 0x71, +]; + +/// Whether `bytes` carry a C2PA manifest store. This is presence only: +/// whether the store is well placed and intact is the signer's to check, +/// see [`MediaJob::optimizer_output`]. +fn carries_c2pa_manifest_store(bytes: &[u8]) -> bool { + bytes + .windows(C2PA_STORE_DESCRIPTION.len()) + .any(|window| window == C2PA_STORE_DESCRIPTION) +} + +/// The most boxes [`bmff_boxes`] reads at one level before it gives up. +const MAX_BMFF_BOXES: usize = 4096; + +/// The type and body range of each ISO BMFF box laid end to end in +/// `bytes`, or `None` where they are not well formed, or are more than +/// [`MAX_BMFF_BOXES`]. A size of `1` reads a 64-bit size after the type, +/// and a size of `0` runs the box to the end of `bytes`. +fn bmff_boxes(bytes: &[u8]) -> Option)>> { + let mut boxes = Vec::new(); + let mut offset = 0_usize; + while offset < bytes.len() { + if boxes.len() == MAX_BMFF_BOXES { + return None; + } + let header = bytes.get(offset..offset.checked_add(8)?)?; + let size32 = u32::from_be_bytes(header.get(0..4)?.try_into().ok()?); + let kind: [u8; 4] = header.get(4..8)?.try_into().ok()?; + let (header_len, size) = match size32 { + 0 => (8, bytes.len() - offset), + 1 => { + let large = bytes.get(offset + 8..offset.checked_add(16)?)?; + let size = u64::from_be_bytes(large.try_into().ok()?); + (16, usize::try_from(size).ok()?) + } + size => (8, usize::try_from(size).ok()?), + }; + if size < header_len { + return None; + } + let end = offset.checked_add(size)?; + if end > bytes.len() { + return None; + } + boxes.push((kind, offset + header_len..end)); + offset = end; + } + Some(boxes) +} + +/// Whether `bytes` are a part of a fragmented ISO BMFF stream, which is +/// never signed alone: its provenance, such as C2PA verifiable segment +/// info, binds the stream's init segment and media segments together, so +/// a manifest added to one part would compete with the stream's. +/// +/// A media segment starts with `styp`, or has a `moof` before its first +/// `mdat`, and has no `moov`. An init segment starts with `ftyp`, has a +/// `moov` that holds an `mvex`, and has no `mdat`. A progressive file, +/// whose `moov` holds no `mvex` or which carries an `mdat` beside its +/// `moov`, is not a part, and neither is anything not well formed. +fn is_fragmented_stream_part(bytes: &[u8]) -> bool { + let Some(boxes) = bmff_boxes(bytes) else { + return false; + }; + let Some((first, _)) = boxes.first() else { + return false; + }; + let moov = boxes.iter().find(|(kind, _)| kind == b"moov"); + let first_mdat = boxes.iter().position(|(kind, _)| kind == b"mdat"); + let moof_before_mdat = boxes + .iter() + .take(first_mdat.unwrap_or(boxes.len())) + .any(|(kind, _)| kind == b"moof"); + let media_segment = (first == b"styp" || moof_before_mdat) && moov.is_none(); + let init_segment = first == b"ftyp" + && first_mdat.is_none() + && moov.is_some_and(|(_, body)| { + bytes + .get(body.clone()) + .and_then(bmff_boxes) + .is_some_and(|children| children.iter().any(|(kind, _)| kind == b"mvex")) + }); + media_segment || init_segment +} + +/// Whether a `Cache-Control` line names `directive`. +pub(crate) fn cache_control_has(headers: &HeaderMap, directive: &str) -> bool { + headers + .get_all(header::CACHE_CONTROL) + .iter() + .filter_map(|value| value.to_str().ok()) + .flat_map(|value| value.split(',')) + .map(|part| part.split('=').next().unwrap_or_default().trim()) + .any(|name| name.eq_ignore_ascii_case(directive)) +} + +fn header_text<'a>(headers: &'a HeaderMap, name: &HeaderName) -> &'a str { + headers + .get(name) + .and_then(|value| value.to_str().ok()) + .unwrap_or_default() +} + +/// The origin's `Content-Length`, where it sent one that parses. +fn declared_length(headers: &HeaderMap) -> Option { + digits(header_text(headers, &header::CONTENT_LENGTH).trim()) +} + +/// The complete length a `206`'s `Content-Range: bytes a-b/total` names. +fn content_range_total(headers: &HeaderMap) -> Option { + let value = header_text(headers, &header::CONTENT_RANGE).trim(); + let (unit, range) = value.split_once(' ')?; + if !unit.eq_ignore_ascii_case("bytes") { + return None; + } + digits(range.rsplit_once('/')?.1.trim()) +} + +fn to_u64(value: usize) -> u64 { + u64::try_from(value).unwrap_or(u64::MAX) +} + +/// A decimal number of ASCII digits alone, which `str::parse` would also +/// take with a leading `+`. +fn digits(value: &str) -> Option { + if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + value.parse().ok() +} + +/// A body read up to a limit. +enum Captured { + /// The whole body, within the limit. + Whole(Bytes), + /// A body over the limit, or one whose stream failed, rebuilt from what + /// was read and what was not, so the reader receives what it would have. + Partial(EdgeBody), +} + +/// Reads `body` whole when it is no larger than `limit`. +async fn capture(body: EdgeBody, limit: usize) -> Captured { + match body { + EdgeBody::Once(bytes) if bytes.len() <= limit => Captured::Whole(bytes), + once @ EdgeBody::Once(_) => Captured::Partial(once), + EdgeBody::Stream(mut stream) => { + let mut chunks = Vec::new(); + let mut total = 0_usize; + while let Some(item) = stream.next().await { + match item { + Ok(chunk) => { + total = total.saturating_add(chunk.len()); + chunks.push(chunk); + if total > limit { + let read = futures::stream::iter(chunks.into_iter().map(Ok)); + return Captured::Partial(EdgeBody::Stream( + read.chain(stream).boxed_local(), + )); + } + } + Err(error) => { + let read = chunks + .into_iter() + .map(Ok) + .chain(std::iter::once(Err(error))); + return Captured::Partial(EdgeBody::Stream( + futures::stream::iter(read).boxed_local(), + )); + } + } + } + let mut whole = BytesMut::with_capacity(total); + for chunk in chunks { + whole.extend_from_slice(&chunk); + } + Captured::Whole(whole.freeze()) + } + } +} + +/// How a request is answered from a signed representation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Outcome { + /// `200` with the whole representation. + Full, + /// `304`. + NotModified, + /// `412`. + PreconditionFailed, + /// `206` with bytes `first` to `last`, inclusive. + Partial { first: u64, last: u64 }, + /// `416`. + Unsatisfiable, +} + +/// Evaluates the reader's preconditions and range against a representation +/// in the order RFC 9110 section 13.2.2 gives. `etag` and `last_modified` +/// are the representation's own validators. A signed representation has no +/// modification date, so it passes `None` and `If-Modified-Since` and +/// `If-Unmodified-Since` are ignored; an `If-Range` date never holds. A +/// `Range` is honored on a `GET` alone, for one range. Several ranges are +/// answered with the whole representation, which the RFC allows. +fn evaluate( + method: &Method, + preconditions: &ReaderPreconditions, + etag: Option<&str>, + last_modified: Option, + length: u64, +) -> Outcome { + let current = etag.and_then(single_entity_tag); + let date_of = + |value: Option<&str>| value.and_then(|value| httpdate::parse_http_date(value.trim()).ok()); + + if let Some(value) = preconditions.if_match.as_deref() { + let matched = match TagList::parse(value) { + TagList::Any => true, + TagList::Tags(tags) => current + .as_ref() + .is_some_and(|current| tags.iter().any(|tag| tag.strong_eq(current))), + }; + if !matched { + return Outcome::PreconditionFailed; + } + } else if let (Some(since), Some(modified)) = ( + date_of(preconditions.if_unmodified_since.as_deref()), + last_modified, + ) && modified > since + { + return Outcome::PreconditionFailed; + } + + if let Some(value) = preconditions.if_none_match.as_deref() { + let matched = match TagList::parse(value) { + TagList::Any => true, + TagList::Tags(tags) => current + .as_ref() + .is_some_and(|current| tags.iter().any(|tag| tag.opaque == current.opaque)), + }; + if matched { + return if *method == Method::GET || *method == Method::HEAD { + Outcome::NotModified + } else { + Outcome::PreconditionFailed + }; + } + } else if (*method == Method::GET || *method == Method::HEAD) + && let (Some(since), Some(modified)) = ( + date_of(preconditions.if_modified_since.as_deref()), + last_modified, + ) + && modified <= since + { + return Outcome::NotModified; + } + + if *method != Method::GET { + return Outcome::Full; + } + let Some(range) = preconditions.range.as_deref() else { + return Outcome::Full; + }; + if let Some(if_range) = preconditions.if_range.as_deref() { + let holds = single_entity_tag(if_range) + .zip(current.as_ref()) + .is_some_and(|(tag, current)| tag.strong_eq(current)); + if !holds { + return Outcome::Full; + } + } + match parse_single_range(range, length) { + RangeRequest::Ignore => Outcome::Full, + RangeRequest::Satisfiable { first, last } => Outcome::Partial { first, last }, + RangeRequest::Unsatisfiable => Outcome::Unsatisfiable, + } +} + +/// One entity tag. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct EntityTag<'a> { + weak: bool, + opaque: &'a str, +} + +impl EntityTag<'_> { + fn strong_eq(&self, other: &EntityTag<'_>) -> bool { + !self.weak && !other.weak && self.opaque == other.opaque + } +} + +/// The entity tag at the start of `input` and what follows it. +fn split_entity_tag(input: &str) -> Option<(EntityTag<'_>, &str)> { + let (weak, quoted) = match input.strip_prefix("W/") { + Some(rest) => (true, rest), + None => (false, input), + }; + let body = quoted.strip_prefix('"')?; + let end = body.find('"')?; + Some(( + EntityTag { + weak, + opaque: &body[..end], + }, + &body[end + 1..], + )) +} + +/// A header holding exactly one entity tag, such as `ETag`. +fn single_entity_tag(value: &str) -> Option> { + let (tag, rest) = split_entity_tag(value.trim())?; + rest.trim().is_empty().then_some(tag) +} + +/// An `If-Match` or `If-None-Match` value. +enum TagList<'a> { + Any, + Tags(Vec>), +} + +impl<'a> TagList<'a> { + /// The tags of a list, as far as it parses. A tag may hold a comma, so + /// the list is read tag by tag. + fn parse(value: &'a str) -> Self { + if value.trim() == "*" { + return Self::Any; + } + let mut tags = Vec::new(); + let mut rest = value; + loop { + rest = rest.trim_start_matches([',', ' ', '\t']); + if rest.is_empty() { + break; + } + let Some((tag, after)) = split_entity_tag(rest) else { + break; + }; + tags.push(tag); + rest = after; + } + Self::Tags(tags) + } +} + +/// What a `Range` header asks of a representation of known length. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RangeRequest { + /// Not one byte range this answers, so the whole representation goes. + Ignore, + Satisfiable { + first: u64, + last: u64, + }, + Unsatisfiable, +} + +fn parse_single_range(value: &str, length: u64) -> RangeRequest { + let Some((unit, set)) = value.trim().split_once('=') else { + return RangeRequest::Ignore; + }; + if !unit.trim().eq_ignore_ascii_case("bytes") { + return RangeRequest::Ignore; + } + let mut specs = set + .split(',') + .map(str::trim) + .filter(|spec| !spec.is_empty()); + let (Some(spec), None) = (specs.next(), specs.next()) else { + return RangeRequest::Ignore; + }; + let Some((first, last)) = spec.split_once('-') else { + return RangeRequest::Ignore; + }; + let (first, last) = (first.trim(), last.trim()); + if first.is_empty() { + let Some(suffix) = digits(last) else { + return RangeRequest::Ignore; + }; + if suffix == 0 || length == 0 { + return RangeRequest::Unsatisfiable; + } + return RangeRequest::Satisfiable { + first: length.saturating_sub(suffix), + last: length - 1, + }; + } + let Some(first) = digits(first) else { + return RangeRequest::Ignore; + }; + let last = if last.is_empty() { + None + } else { + match digits(last) { + Some(last) if last >= first => Some(last), + _ => return RangeRequest::Ignore, + } + }; + if first >= length { + return RangeRequest::Unsatisfiable; + } + RangeRequest::Satisfiable { + first, + last: last.map_or(length - 1, |last| last.min(length - 1)), + } +} + +#[cfg(test)] +pub(crate) mod test_support; + +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod lookup_tests; diff --git a/crates/trusted-server-core/src/media_signing/large.rs b/crates/trusted-server-core/src/media_signing/large.rs new file mode 100644 index 0000000000..901c30cbe8 --- /dev/null +++ b/crates/trusted-server-core/src/media_signing/large.rs @@ -0,0 +1,1706 @@ +//! The large-media lane: a representation larger than a signer's bounded +//! limit, signed without core ever holding it whole. +//! +//! The lane signs an unchanged source only. The bytes the reader receives +//! are the origin's own, so the origin's representation keeps its own +//! validators, length and range handling, and the signer only adds headers, +//! such as a manifest `Link`. Core adds a `Repr-Digest` of the whole source. +//! +//! Core admits an origin answer to the lane only where it can pin one +//! version of the source and keep the outcome: a shared request, an +//! identity-coded answer the origin keeps shareable for a while and lets +//! be transformed, a cache that keeps values, a single strong `ETag`, a +//! known total length above the signer's bounded limit and within its +//! large limit, a platform that streams response bodies and offers a +//! timer, and no C2PA manifest `Link` of the origin's own. Before anything +//! is hashed, core also requires a public address to name the source by: +//! the fetched address where it is plain `https` and core does not sign +//! the request for it, or else the reader's query-free `https` address. +//! An answer that fails any of these goes to the reader as the origin sent +//! it, validators and all; an answer with no strong `ETag` is never given +//! an invented version. Only a reader's `GET` of an answer whose origin +//! says `Accept-Ranges: bytes` starts the hashing of a version; a `HEAD`, +//! or a `GET` without that, is answered from a kept outcome or goes as it +//! came. +//! +//! Every later read of a version being signed, by core or the signer, asks +//! the origin through the route's own backend and origin signing for +//! exactly one byte range with `If-Match` on the pinned tag, and refuses an +//! answer whose status, `Content-Range`, `Content-Length`, coding or `ETag` +//! does not describe exactly that range of exactly that version. A changed +//! version therefore fails the read, never mixes into a digest or a signed +//! delivery. A version the lane declined has no signature to protect, so +//! where the origin's first answer can no longer be sent its reader gets +//! one fresh origin answer to the request as core first sent it, held by +//! no cache. +//! +//! What core keeps for the lane is the outcome only, never the bytes: for +//! a signed version the whole-source digest, the length and the signer's +//! headers, for a declined one (refused, failed or out of time) that it was +//! declined, briefly. Both are kept under the publisher's prefix, the +//! signer's current [`namespace`](super::MediaSigner::namespace), the +//! reader's address, the origin and the pinned tag, for no longer than the +//! origin's shared freshness. A changed or revoked policy names another +//! namespace, so it never finds an outcome kept under the old one. The +//! hashing and signing run as one load of the loading cache, which runs a +//! key's load once for every caller asking for it at the same time. + +use std::fmt; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; + +use base64::Engine as _; +use bytes::{Bytes, BytesMut}; +use edgezero_core::body::Body as EdgeBody; +use futures::StreamExt as _; +use futures::future::Either; +use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, StatusCode, header}; +use sha2::{Digest as _, Sha256}; +use web_time::Instant; + +use super::{ + ASSETS_CACHE, Delivery, KeptReader, MediaClaim, MediaSigner, MediaSigningFailure, OriginFacts, + Outcome, VariantIdentity, carries_manifest_link, digits, evaluate, header_text, + is_reserved_header, single_entity_tag, source_reference, strip_representation_preconditions, + to_u64, +}; +use crate::middleware::ResponseHeader; +use crate::origin_auth::OriginSigning; +use crate::platform::{ + LoadError, LoadedValue, Loader, PlatformHttpRequest, RuntimeServices, StoreKey, +}; + +/// What [`MediaSigner::sign_large`](super::MediaSigner::sign_large) returns. +pub type LargeMediaSigning<'a> = + Pin> + 'a>>; + +/// One large, unchanged representation a signer is asked to sign. +#[derive(Debug, Clone, Copy)] +pub struct LargeMediaJob<'a> { + /// The address the reader asked for, `scheme://host/path`, without its + /// query. + pub reader_url: &'a str, + /// The origin's `Content-Type`, as it sent it. + pub content_type: &'a str, + /// The pinned version of the source, read at offsets. + pub source: &'a VersionedMediaSource<'a>, + /// SHA-256 of every byte of the pinned version, lower case hex, which + /// core computed in bounded chunks and checked against its length. The + /// bytes the reader receives are these bytes. + pub source_sha256: &'a str, + /// The address core fetched the source from, exactly as fetched, where + /// it is `https` and carries no user information, query or fragment. + pub source_reference: Option<&'a str>, + /// The reader's address, where it is `https` and the reader's request + /// carried no query. It names the source only because this lane + /// delivers the source unchanged. + pub reader_reference: Option<&'a str>, + /// The lower case host of the address core fetched the source from, + /// with no scheme, port, path, query or user information. + pub source_origin_host: &'a str, + /// What is left of the signer's + /// [`max_large_media_duration`](super::MediaSigner::max_large_media_duration) + /// after core's hashing. Core drops the signing when it is spent. + pub remaining: Duration, +} + +/// What a signer returns for a large representation: headers only, since +/// the bytes are the origin's. +#[derive(Debug, Clone)] +pub struct LargeSignedMedia { + /// Headers written on every answer of the signed version: `200`, `206`, + /// `HEAD` and `304`. A `Link` is added to the origin's own links, and + /// any other replaces the origin's header of that name. A name core + /// owns, see [`is_reserved_header`], fails the signing. + pub headers: Vec, + /// The policy and credential context the source was signed under, as + /// [`MediaSigner::namespace`](super::MediaSigner::namespace) gives it + /// while that context is current. Core keeps the metadata under it. + pub namespace: String, +} + +/// One version of a source: its strong entity tag and its length. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceVersion { + /// The strong tag, quoted, as `"opaque"`. + etag: String, + length: u64, +} + +impl SourceVersion { + /// The version a single strong entity tag `etag` and a length name, or + /// `None` where `etag` is weak, empty or not one tag. + #[must_use] + pub fn new(etag: &str, length: u64) -> Option { + let tag = single_entity_tag(etag)?; + (!tag.weak && !tag.opaque.is_empty()).then(|| Self { + etag: format!("\"{}\"", tag.opaque), + length, + }) + } + + /// The version an origin answer's headers name, where they carry + /// exactly one strong `ETag`. + fn of(headers: &HeaderMap, length: u64) -> Option { + let mut tags = headers.get_all(header::ETAG).iter(); + let (Some(tag), None) = (tags.next(), tags.next()) else { + return None; + }; + Self::new(tag.to_str().ok()?, length) + } + + /// The strong entity tag, quoted. + #[must_use] + pub fn etag(&self) -> &str { + &self.etag + } + + /// The length in bytes. + #[must_use] + pub fn length(&self) -> u64 { + self.length + } + + /// Whether an answer's headers carry this version's tag, strongly. + fn matches(&self, headers: &HeaderMap) -> bool { + let mut tags = headers.get_all(header::ETAG).iter(); + let (Some(tag), None) = (tags.next(), tags.next()) else { + return false; + }; + tag.to_str() + .ok() + .and_then(single_entity_tag) + .is_some_and(|tag| { + !tag.weak && self.etag.get(1..self.etag.len() - 1) == Some(tag.opaque) + }) + } +} + +/// Why a read of a [`VersionedMediaSource`] failed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] +pub enum MediaSourceError { + /// The origin no longer serves the pinned version: it answered `412` to + /// `If-Match`, or with a missing, weak or different `ETag`. Nothing + /// read from this source may be published. + #[display("the origin no longer serves the pinned version")] + VersionChanged, + /// The read reaches past the pinned version's length. + #[display("the read reaches past the pinned version")] + OutOfBounds, + /// The origin answered a pinned read with something other than exactly + /// the bytes asked for. + #[display("the origin answered a pinned read wrongly: {reason}")] + InvalidAnswer { + /// A short code naming what was wrong. + reason: &'static str, + }, + /// The read could not be sent, or its body failed or ended early. + #[display("a pinned read could not be completed")] + Transport, +} + +impl core::error::Error for MediaSourceError {} + +impl MediaSourceError { + /// A short code naming the failure, for logs and refusals. + #[must_use] + pub fn code(&self) -> &'static str { + match self { + Self::VersionChanged => "source_version_changed", + Self::OutOfBounds => "source_read_out_of_bounds", + Self::InvalidAnswer { reason } => reason, + Self::Transport => "source_transport", + } + } +} + +/// How core asks the origin for one more byte range of a source: the +/// address, the request's headers as they stood before it was signed, the +/// backend and the origin's signing. +#[derive(Clone, Copy)] +pub(crate) struct PinnedOrigin<'a> { + /// The address, exactly as fetched and signed. + pub(crate) url: &'a url::Url, + /// The origin request's headers before signing. + pub(crate) headers: &'a HeaderMap, + /// The backend the origin request went to. + pub(crate) backend_name: &'a str, + /// The origin's signing, applied fresh to every read. + pub(crate) auth: Option<&'a OriginSigning>, + /// Headers a signed read must not carry, removed before core writes its + /// own conditions and signs. + pub(crate) unsigned_headers: &'a [HeaderName], + /// Publisher URL mapping for a fresh unsigned response; asset proxies keep + /// the origin's navigation targets as on their ordinary response path. + pub(crate) response_header_rewrite: + Option>, +} + +/// One pinned version of a source, read at offsets from the origin. +/// +/// Every read asks the origin for exactly one byte range with `If-Match` +/// on the pinned tag, through the route's own backend and origin signing, +/// and fails unless the answer is exactly that range of that version. +pub struct VersionedMediaSource<'a> { + services: &'a RuntimeServices, + origin: PinnedOrigin<'a>, + version: SourceVersion, +} + +impl fmt::Debug for VersionedMediaSource<'_> { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("VersionedMediaSource") + .field("version", &self.version) + .finish_non_exhaustive() + } +} + +impl<'a> VersionedMediaSource<'a> { + /// A source read from `url` through `backend_name` with `headers`, an + /// origin that signs no request, pinned to `version`. + #[must_use] + pub fn new( + services: &'a RuntimeServices, + url: &'a url::Url, + headers: &'a HeaderMap, + backend_name: &'a str, + version: SourceVersion, + ) -> Self { + Self { + services, + origin: PinnedOrigin { + url, + headers, + backend_name, + auth: None, + unsigned_headers: &[], + response_header_rewrite: None, + }, + version, + } + } + + /// The version every read is pinned to. + #[must_use] + pub fn version(&self) -> &SourceVersion { + &self.version + } + + /// The pinned version's length in bytes. + #[must_use] + pub fn size(&self) -> u64 { + self.version.length + } + + /// Fills `destination` with the bytes at `offset` of the pinned version. + /// + /// # Errors + /// + /// [`MediaSourceError::OutOfBounds`] where the read reaches past the + /// version, [`MediaSourceError::VersionChanged`] where the origin no + /// longer serves it, and the other variants where the origin's answer + /// is not exactly the range asked for or does not arrive whole. + /// + /// # Performance + /// + /// One origin request per call, streamed into `destination`. + pub async fn read_exact_at( + &self, + offset: u64, + destination: &mut [u8], + ) -> Result<(), MediaSourceError> { + let length = to_u64(destination.len()); + let mut filled = 0_usize; + let mut visit = |chunk: &[u8]| { + let end = filled.saturating_add(chunk.len()); + if let Some(slot) = destination.get_mut(filled..end) { + slot.copy_from_slice(chunk); + } + filled = end; + }; + self.stream_range(offset, length, &mut visit).await + } + + /// Hands `visit` the bytes from `offset` for `length` bytes of the + /// pinned version, in order, in the chunks the origin sends. + /// + /// # Errors + /// + /// As [`read_exact_at`](Self::read_exact_at). `visit` may have seen + /// some of the bytes before a read fails. + /// + /// # Performance + /// + /// One origin request; nothing is held beyond one chunk. + pub async fn for_each_chunk( + &self, + offset: u64, + length: u64, + mut visit: F, + ) -> Result<(), MediaSourceError> { + self.stream_range(offset, length, &mut visit).await + } + + /// Whether the origin still serves the pinned version, by a pinned + /// read of its first byte. A signer asks this last before it publishes. + /// + /// # Errors + /// + /// As [`read_exact_at`](Self::read_exact_at). + pub async fn confirm_current(&self) -> Result<(), MediaSourceError> { + let mut first = [0_u8; 1]; + self.read_exact_at(0, &mut first).await + } + + /// The same source read through `origin`, which may sign its reads. + pub(crate) fn pinned( + services: &'a RuntimeServices, + origin: PinnedOrigin<'a>, + version: SourceVersion, + ) -> Self { + Self { + services, + origin, + version, + } + } + + /// Streams `length` bytes from `offset` to `visit`. + async fn stream_range( + &self, + offset: u64, + length: u64, + visit: &mut F, + ) -> Result<(), MediaSourceError> { + if length == 0 { + return Ok(()); + } + let last = offset + .checked_add(length - 1) + .filter(|last| *last < self.version.length) + .ok_or(MediaSourceError::OutOfBounds)?; + let body = self.send_pinned(offset, last).await?; + drain(body, length, visit).await + } + + /// Asks the origin for bytes `first` to `last` of the pinned version + /// and returns the body of an answer that is exactly that. + async fn send_pinned(&self, first: u64, last: u64) -> Result { + let request = self.pinned_request(first, last)?; + // The platform's error may name the address, which can carry a + // credential, so it is not logged. + let answer = self + .services + .http_client() + .send(request) + .await + .map_err(|_| MediaSourceError::Transport)?; + let (parts, body) = answer.response.into_parts(); + check_pinned(&self.version, parts.status, &parts.headers, first, last)?; + Ok(body) + } + + /// The origin request for bytes `first` to `last`: the request as it + /// stood before signing, with every condition the reader sent removed, + /// and only core's own `If-Match`, `Range` and identity coding written + /// before it is signed afresh. + fn pinned_request( + &self, + first: u64, + last: u64, + ) -> Result { + let mut headers = self.origin.base_headers(); + strip_representation_preconditions(&mut headers); + headers.insert( + header::ACCEPT_ENCODING, + HeaderValue::from_static("identity"), + ); + headers.insert( + header::IF_MATCH, + HeaderValue::from_str(&self.version.etag).map_err(|_| { + MediaSourceError::InvalidAnswer { + reason: "etag_not_a_header", + } + })?, + ); + headers.insert( + header::RANGE, + HeaderValue::from_str(&format!("bytes={first}-{last}")) + .expect("should build a byte range"), + ); + let request = self + .origin + .signed_request(&Method::GET, headers) + .map_err(|()| MediaSourceError::Transport)?; + Ok( + if self.services.http_client().supports_streaming_responses() { + request.with_stream_response() + } else { + request.with_max_response_body_bytes( + usize::try_from(last - first + 1).unwrap_or(usize::MAX), + ) + }, + ) + } +} + +impl PinnedOrigin<'_> { + /// The origin request's headers as they stood before signing, less + /// the headers a signed request must not carry where core signs it. + fn base_headers(&self) -> HeaderMap { + let mut headers = self.headers.clone(); + if self.auth.is_some() { + for name in self.unsigned_headers { + headers.remove(name); + } + } + headers + } + + /// A `method` request to the origin carrying `headers`, signed afresh + /// where the origin signs requests. Nothing is written after the + /// signature. + fn signed_request( + &self, + method: &Method, + mut headers: HeaderMap, + ) -> Result { + if let Some(auth) = self.auth { + auth.remove_namespace_headers(&mut headers); + auth.sign(method, self.url, &mut headers).map_err(|_| ())?; + } + let mut request = Request::builder() + .method(method.clone()) + .uri(self.url.as_str()) + .body(EdgeBody::empty()) + .map_err(|_| ())?; + *request.headers_mut() = headers; + Ok(PlatformHttpRequest::new(request, self.backend_name)) + } +} + +/// Checks that an answer to a pinned read of bytes `first` to `last` is +/// exactly that range of `version`. +fn check_pinned( + version: &SourceVersion, + status: StatusCode, + headers: &HeaderMap, + first: u64, + last: u64, +) -> Result<(), MediaSourceError> { + if status == StatusCode::PRECONDITION_FAILED { + return Err(MediaSourceError::VersionChanged); + } + if status != StatusCode::OK && status != StatusCode::PARTIAL_CONTENT { + return Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_status", + }); + } + if !version.matches(headers) { + return Err(MediaSourceError::VersionChanged); + } + let encoding = header_text(headers, &header::CONTENT_ENCODING).trim(); + if !encoding.is_empty() && !encoding.eq_ignore_ascii_case("identity") { + return Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_content_encoded", + }); + } + let whole = first == 0 && last.checked_add(1) == Some(version.length); + if status == StatusCode::PARTIAL_CONTENT { + if content_range(headers) != Some((first, last, version.length)) { + return Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_content_range", + }); + } + } else if !whole { + return Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_status", + }); + } + let declared = header_text(headers, &header::CONTENT_LENGTH).trim(); + if !declared.is_empty() && digits(declared) != Some(last - first + 1) { + return Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_content_length", + }); + } + Ok(()) +} + +/// The first byte, last byte and complete length a `206`'s +/// `Content-Range: bytes first-last/length` names. +fn content_range(headers: &HeaderMap) -> Option<(u64, u64, u64)> { + let value = header_text(headers, &header::CONTENT_RANGE).trim(); + let (unit, range) = value.split_once(' ')?; + if !unit.eq_ignore_ascii_case("bytes") { + return None; + } + let (span, total) = range.trim().split_once('/')?; + let (first, last) = span.split_once('-')?; + Some(( + digits(first.trim())?, + digits(last.trim())?, + digits(total.trim())?, + )) +} + +/// Hands `visit` every chunk of `body`, which must hold exactly `expected` +/// bytes. A chunk that would pass `expected` is refused before `visit` +/// sees it. A stream that fails or ends early is a +/// [`MediaSourceError::Transport`]. +async fn drain( + body: EdgeBody, + expected: u64, + visit: &mut F, +) -> Result<(), MediaSourceError> { + match body { + EdgeBody::Once(bytes) => { + if to_u64(bytes.len()) != expected { + return Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_body_length", + }); + } + visit(&bytes); + Ok(()) + } + EdgeBody::Stream(mut stream) => { + let mut seen = 0_u64; + while let Some(item) = stream.next().await { + let Ok(chunk) = item else { + return Err(MediaSourceError::Transport); + }; + let next = seen.saturating_add(to_u64(chunk.len())); + if next > expected { + return Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_overlong_body", + }); + } + visit(&chunk); + seen = next; + } + if seen == expected { + Ok(()) + } else { + Err(MediaSourceError::Transport) + } + } + } +} + +/// SHA-256 of every byte of `source`'s pinned version, read once by one +/// pinned read in bounded chunks. A read that fails fails the digest; it is +/// never restarted or resumed. +async fn whole_digest(source: &VersionedMediaSource<'_>) -> Result<[u8; 32], MediaSourceError> { + let mut hasher = Sha256::new(); + let mut visit = |chunk: &[u8]| hasher.update(chunk); + source.stream_range(0, source.size(), &mut visit).await?; + let mut digest = [0_u8; 32]; + digest.copy_from_slice(&hasher.finalize()); + Ok(digest) +} + +/// `body` as sent to the reader, refused where it holds more or fewer than +/// `expected` bytes, so a reader never receives bytes of another length +/// under headers that describe this one. +fn exact_body(body: EdgeBody, expected: u64) -> Result { + match body { + EdgeBody::Once(bytes) if to_u64(bytes.len()) == expected => Ok(EdgeBody::Once(bytes)), + EdgeBody::Once(_) => Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_body_length", + }), + EdgeBody::Stream(stream) => Ok(EdgeBody::from_stream(futures::stream::unfold( + (stream, 0_u64, false), + move |(mut stream, sent, finished)| async move { + if finished { + return None; + } + match stream.next().await { + Some(Ok(chunk)) => { + let sent = sent.saturating_add(to_u64(chunk.len())); + if sent > expected { + let error = MediaSourceError::InvalidAnswer { + reason: "pinned_read_overlong_body", + }; + Some((Err(error), (stream, sent, true))) + } else { + Some((Ok(chunk), (stream, sent, false))) + } + } + Some(Err(_)) => Some((Err(MediaSourceError::Transport), (stream, sent, true))), + None if sent == expected => None, + None => Some((Err(MediaSourceError::Transport), (stream, sent, true))), + } + }, + ))), + } +} + +/// The first bytes of kept metadata for a signed version, naming its +/// layout. +const SIGNED_MAGIC: &[u8; 4] = b"TSL1"; + +/// The whole of the kept record for a version the lane declined: refused +/// by the signer, failed to read or hash, or out of time. +const DECLINED_MAGIC: &[u8; 4] = b"TSL0"; + +/// The longest metadata for a signed version is kept, whatever the origin +/// allows. +const SIGNED_MAX_LIFETIME: Duration = Duration::from_secs(3600); + +/// The longest a declined version is kept, so a source, policy or signer +/// that changes is tried again soon, while a burst of requests for one +/// version never hashes it once per request. +const DECLINED_MAX_LIFETIME: Duration = Duration::from_secs(60); + +/// How long a lookup may take before the origin's own answer, held unread +/// meanwhile, is no longer trusted to stream, and the bytes are read again +/// pinned. A lookup that waits on another caller's load can take as long as +/// that load. +const ORIGIN_BODY_MAX_WAIT: Duration = Duration::from_secs(1); + +/// The most signer headers kept metadata may hold. +const LARGE_MAX_HEADERS: usize = 64; + +/// What core keeps for a signed large version: never its bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +struct LargeRecord { + length: u64, + sha256: [u8; 32], + headers: Vec, +} + +/// What core keeps for one large version under one namespace. +#[derive(Debug, Clone, PartialEq, Eq)] +enum KeptOutcome { + /// The version was signed; its answers carry these headers. + Signed(LargeRecord), + /// The version was not signed; its answers go as the origin made them. + Declined, +} + +/// Metadata for a signed version as written: [`SIGNED_MAGIC`], the length, +/// the digest, the header count, then each header's name and value with +/// their lengths. +fn encode_large(record: &LargeRecord) -> Bytes { + let mut out = BytesMut::with_capacity(128); + out.extend_from_slice(SIGNED_MAGIC); + out.extend_from_slice(&record.length.to_be_bytes()); + out.extend_from_slice(&record.sha256); + let count = record.headers.len().min(LARGE_MAX_HEADERS); + out.extend_from_slice(&u32::try_from(count).unwrap_or(u32::MAX).to_be_bytes()); + for (name, value) in record.headers.iter().take(count) { + for field in [name.as_str().as_bytes(), value.as_bytes()] { + out.extend_from_slice(&u32::try_from(field.len()).unwrap_or(u32::MAX).to_be_bytes()); + out.extend_from_slice(field); + } + } + out.freeze() +} + +/// A kept outcome as [`encode_large`] or [`DECLINED_MAGIC`] wrote it, or +/// `None` for any other bytes. +fn decode_kept_outcome(bytes: &[u8]) -> Option { + if bytes == DECLINED_MAGIC.as_slice() { + return Some(KeptOutcome::Declined); + } + let mut reader = KeptReader { + rest: bytes.strip_prefix(SIGNED_MAGIC.as_slice())?, + }; + let length = reader.number()?; + let sha256: [u8; 32] = reader.take(32)?.try_into().ok()?; + let count = reader.length()?; + if count > LARGE_MAX_HEADERS { + return None; + } + let mut headers = Vec::with_capacity(count); + for _ in 0..count { + let name_length = reader.length()?; + let name = HeaderName::from_bytes(reader.take(name_length)?).ok()?; + let value_length = reader.length()?; + let value = HeaderValue::from_bytes(reader.take(value_length)?).ok()?; + if is_reserved_header(&name) { + return None; + } + headers.push((name, value)); + } + reader + .rest + .is_empty() + .then_some(KeptOutcome::Signed(LargeRecord { + length, + sha256, + headers, + })) +} + +/// Writes a signed version's headers on an answer of it: the signer's, +/// with a `Link` added to the origin's, and the whole source's +/// `Repr-Digest`. +fn decorate(headers: &mut HeaderMap, record: &LargeRecord) { + for (name, value) in &record.headers { + if name == header::LINK { + headers.append(name.clone(), value.clone()); + } else { + headers.insert(name.clone(), value.clone()); + } + } + let digest = format!( + "sha-256=:{}:", + base64::engine::general_purpose::STANDARD.encode(record.sha256) + ); + headers.insert( + HeaderName::from_static("repr-digest"), + HeaderValue::from_str(&digest).expect("should build a base64 digest"), + ); +} + +/// The answer where a signed or meant-to-be-signed version can no longer +/// be read: nothing of another version is sent in its place. +fn refusal() -> Response { + Response::builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .header(header::CACHE_CONTROL, "no-store") + .header(header::RETRY_AFTER, "1") + .header(header::CONTENT_LENGTH, 0) + .body(EdgeBody::empty()) + .expect("should build a refusal") +} + +/// The headers a fresh origin answer for an unsigned version brings: its +/// status framing, type, coding, validators and redirect/authentication +/// semantics. Every other header stays as core prepared it for the reader. +const FRESH_REPRESENTATION_HEADERS: [&str; 16] = [ + "content-type", + "content-location", + "content-length", + "content-encoding", + "content-range", + "accept-ranges", + "etag", + "last-modified", + "repr-digest", + "content-digest", + "digest", + "content-md5", + "transfer-encoding", + "location", + "www-authenticate", + "retry-after", +]; + +/// Whether the origin says it serves byte ranges of this representation, +/// which every pinned read relies on. +fn advertises_byte_ranges(headers: &HeaderMap) -> bool { + headers + .get_all(header::ACCEPT_RANGES) + .iter() + .filter_map(|value| value.to_str().ok()) + .flat_map(|value| value.split(',')) + .any(|unit| unit.trim().eq_ignore_ascii_case("bytes")) +} + +/// The answer where the origin could not be asked again for an unsigned +/// version. +fn origin_unreachable() -> Response { + Response::builder() + .status(StatusCode::BAD_GATEWAY) + .header(header::CACHE_CONTROL, "no-store") + .header(header::CONTENT_LENGTH, 0) + .body(EdgeBody::empty()) + .expect("should build a gateway failure") +} + +/// An origin answer admitted to the large lane, with the version it pins. +pub(crate) struct LargeAnswer { + response: Response, + version: SourceVersion, + /// How long the origin let its answer be kept, shared, which bounds + /// how long its outcome is kept. + keep_for: Duration, +} + +impl LargeAnswer { + /// The origin's answer as it came, for a caller that cannot say how to + /// ask the origin again, sent unsigned. + pub(crate) fn into_origin_response(self) -> Response { + self.response + } +} + +/// The one wall-clock budget for a large source's namespace resolution, +/// cache waits, hashing and signing. +#[derive(Debug, Clone, Copy)] +struct LargeBudget { + started: Instant, + total: Duration, +} + +impl LargeBudget { + /// What is left of the budget now. + fn remaining(self) -> Duration { + self.total.saturating_sub(self.started.elapsed()) + } +} + +/// The public addresses a large source may be named by, worked out before +/// anything is hashed. +#[derive(Debug, Clone, PartialEq, Eq)] +struct LargeReferences { + /// The fetched address, where it is `https`, carries no user + /// information, query or fragment, and core does not sign the request + /// for it: an address core signs is a private one and is never named. + source: Option, + /// The reader's address, where it is `https` and the reader's request + /// carried no query. + reader: Option, +} + +impl LargeReferences { + fn of(identity: &VariantIdentity<'_>, origin_signed: bool) -> Self { + let source = if origin_signed { + None + } else { + source_reference(identity.fetched_url) + .filter(|reference| reference.starts_with("https://")) + .map(str::to_owned) + }; + let reader = (identity.reader_query.is_empty() + && identity.reader_url.starts_with("https://")) + .then(|| identity.reader_url.to_owned()); + Self { source, reader } + } + + fn any(&self) -> bool { + self.source.is_some() || self.reader.is_some() + } +} + +/// Everything the hashing and signing of one large version owns, because +/// the loading cache keeps the load with its key rather than borrowing the +/// request, and runs it once for every caller asking for that key at the +/// same time. +struct LargeLoad { + signer: Arc, + /// The request's services without its connection, since the outcome + /// serves every caller waiting on the load. + services: RuntimeServices, + url: url::Url, + headers: HeaderMap, + backend_name: String, + auth: Option, + unsigned_headers: Vec, + version: SourceVersion, + reader_url: String, + content_type: String, + references: LargeReferences, + /// The namespace the outcome is kept under, which the signer must sign + /// under. + namespace: String, + budget: LargeBudget, + keep_for: Duration, +} + +impl LargeLoad { + fn into_loader(self) -> Loader { + Box::new(move || Box::pin(async move { Ok(self.run().await) })) + } + + /// Hashes and signs within the budget, giving the outcome to keep. A + /// declined outcome is kept briefly too, so the version is not hashed + /// again for every request while it stands. + async fn run(self) -> LoadedValue { + let signer_id = self.signer.signer_id(); + let remaining = self.budget.remaining(); + let outcome = match self.services.timer() { + Some(_) if remaining.is_zero() => { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=timeout" + ); + None + } + Some(timer) => { + match futures::future::select(Box::pin(self.sign()), timer.sleep(remaining)).await { + Either::Left((outcome, _)) => outcome, + Either::Right(((), _)) => { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=timeout" + ); + None + } + } + } + None => { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=no_timer" + ); + None + } + }; + match outcome { + Some(record) => LoadedValue::new(encode_large(&record)) + .with_lifetime(self.keep_for.min(SIGNED_MAX_LIFETIME)), + None => LoadedValue::new(Bytes::from_static(DECLINED_MAGIC)) + .with_lifetime(self.keep_for.min(DECLINED_MAX_LIFETIME)), + } + } + + /// Hashes the pinned version once and asks the signer for it. + async fn sign(&self) -> Option { + let signer_id = self.signer.signer_id(); + let declined = |reason: &str| { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason={reason}" + ); + }; + let origin = PinnedOrigin { + url: &self.url, + headers: &self.headers, + backend_name: &self.backend_name, + auth: self.auth.as_ref(), + unsigned_headers: &self.unsigned_headers, + response_header_rewrite: None, + }; + let source = VersionedMediaSource::pinned(&self.services, origin, self.version.clone()); + let digest = whole_digest(&source) + .await + .map_err(|error| declined(error.code())) + .ok()?; + let Some(origin_host) = self.url.host_str().map(str::to_ascii_lowercase) else { + declined("no_origin_host"); + return None; + }; + let remaining = self.budget.remaining(); + if remaining.is_zero() { + declined("timeout"); + return None; + } + let source_sha256 = hex::encode(digest); + let job = LargeMediaJob { + reader_url: &self.reader_url, + content_type: &self.content_type, + source: &source, + source_sha256: &source_sha256, + source_reference: self.references.source.as_deref(), + reader_reference: self.references.reader.as_deref(), + source_origin_host: &origin_host, + remaining, + }; + let signed = match self.signer.sign_large(&self.services, &job).await { + Ok(signed) + if signed + .headers + .iter() + .any(|(name, _)| is_reserved_header(name)) => + { + declined("reserved_header"); + return None; + } + Ok(signed) if signed.headers.len() > LARGE_MAX_HEADERS => { + declined("too_many_headers"); + return None; + } + Ok(signed) if signed.namespace != self.namespace => { + declined("namespace_changed"); + return None; + } + Ok(signed) => signed, + Err(failure) => { + declined(failure.reason); + return None; + } + }; + log::info!( + "media signing: outcome=signed lane=large signer={signer_id} source_bytes={}", + source.size() + ); + Some(LargeRecord { + length: source.size(), + sha256: digest, + headers: signed.headers, + }) + } +} + +impl MediaClaim { + /// Whether this claim may use the large lane at all: the signer signs + /// large sources and the request is shared. + fn large_lane_open(&self) -> bool { + self.shared && self.signer.max_large_source_bytes() > 0 + } + + /// The version a signable origin answer of `total` bytes, larger than + /// the bounded limit, pins in the large lane, or why it cannot. The + /// answer's coding, privacy, transform rule, type and any manifest + /// `Link` of the origin's own were already checked, see + /// `unsignable_reason`. Every check here is cheap and runs before + /// anything is hashed. + fn large_version( + &self, + services: &RuntimeServices, + headers: &HeaderMap, + total: Option, + facts: &OriginFacts, + ) -> Result { + let ceiling = self.signer.max_large_source_bytes(); + if ceiling == 0 { + return Err("exceeds_limit"); + } + if !self.shared { + return Err("large_request_not_shared"); + } + // A lookup-only signer keeps nothing here and starts no load, so + // neither the outcome's lifetime nor the streaming transport, budget + // and timer a load needs decide whether its result is looked up. + let lookup_only = self.signer.lookup_only(); + // An outcome that cannot be kept would be worked out again for + // every request, each range of one playback included. + if !lookup_only + && (facts.keep_for.is_none() + || !services.loading_cache_for(ASSETS_CACHE).keeps_values()) + { + return Err("large_outcome_not_kept"); + } + let Some(length) = total else { + return Err("unknown_length"); + }; + if length > ceiling { + return Err("exceeds_large_limit"); + } + if !lookup_only { + if !services.http_client().supports_streaming_responses() { + return Err("no_streaming_transport"); + } + if self.signer.max_large_media_duration().is_zero() { + return Err("no_large_media_budget"); + } + if services.timer().is_none() { + return Err("no_timer"); + } + } + SourceVersion::of(headers, length).ok_or("no_strong_etag") + } + + /// What a signable origin answer of `total` bytes, larger than the + /// bounded limit, becomes: a [`Delivery::Large`] where the large lane + /// admits it, else the origin's answer as it came. + pub(super) fn large_or_unsigned( + &self, + services: &RuntimeServices, + response: Response, + total: Option, + facts: &OriginFacts, + ) -> Delivery { + match self.large_version(services, response.headers(), total, facts) { + Ok(version) => Delivery::Large(LargeAnswer { + response, + version, + keep_for: facts.keep_for.unwrap_or_default(), + }), + Err(reason) => { + log::info!( + "media signing: outcome=unsigned signer={} reason={reason}", + self.signer.signer_id() + ); + Delivery::Send(response) + } + } + } + + /// Answers the reader from an origin answer admitted to the large lane. + /// + /// A source no public address can name, or whose signer's namespace + /// cannot be resolved, goes as the origin answered it, before anything + /// is hashed. An outcome kept for the pinned version under the signer's + /// current namespace answers next: a signed one decorates the origin's + /// own answer, a declined one leaves it as it came. A `HEAD`, or an + /// answer whose origin does not say it serves byte ranges, never starts + /// a load and goes as it came. Otherwise the version is hashed and + /// signed once, through the loading cache, so callers asking at the + /// same time share that one load. A signed version is answered by + /// pinned reads, refused where the origin no longer serves it; a + /// declined one by one fresh origin answer to the request as core first + /// sent it, since no signature binds its bytes. `origin` is how the + /// origin is asked again. + pub(crate) async fn deliver_large( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + origin: PinnedOrigin<'_>, + answer: LargeAnswer, + ) -> Response { + let signer_id = self.signer.signer_id(); + if self.signer.lookup_only() { + return self + .deliver_published_large(services, identity, origin, answer) + .await; + } + let budget = LargeBudget { + started: Instant::now(), + total: self.signer.max_large_media_duration(), + }; + let LargeAnswer { + response, + version, + keep_for, + } = answer; + let references = LargeReferences::of(identity, origin.auth.is_some()); + if !references.any() { + log::info!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=no_public_reference" + ); + return response; + } + let Some(timer) = services.timer() else { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=no_timer" + ); + return response; + }; + let remaining = budget.remaining(); + if remaining.is_zero() { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=timeout" + ); + return response; + } + let namespace = match futures::future::select( + self.signer.namespace(services), + timer.sleep(remaining), + ) + .await + { + Either::Left((Ok(namespace), _)) => namespace, + Either::Left((Err(failure), _)) => { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason={failure}" + ); + return response; + } + Either::Right(((), _)) => { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=timeout" + ); + return response; + } + }; + let key = self.large_store_key(services, &namespace, identity, version.etag()); + let looked_up = Instant::now(); + let kept = match futures::future::select( + Box::pin(self.large_kept(services, &key, Some(version.length()))), + timer.sleep(budget.remaining()), + ) + .await + { + Either::Left((kept, _)) => kept, + Either::Right(((), _)) => { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=timeout" + ); + if looked_up.elapsed() <= ORIGIN_BODY_MAX_WAIT { + return response; + } + let (parts, _) = response.into_parts(); + return self.deliver_fresh(services, origin, parts).await; + } + }; + if let Some(kept) = kept { + if looked_up.elapsed() <= ORIGIN_BODY_MAX_WAIT { + log::debug!("media signing: lane=large signer={signer_id} from=kept_outcome"); + return match kept { + KeptOutcome::Signed(record) => { + let (mut parts, body) = response.into_parts(); + decorate(&mut parts.headers, &record); + Response::from_parts(parts, body) + } + KeptOutcome::Declined => response, + }; + } + // The origin's answer waited unread for the lookup, so it is no + // longer trusted to stream. + let (parts, _) = response.into_parts(); + return match kept { + KeptOutcome::Signed(record) => { + let source = VersionedMediaSource::pinned(services, origin, version); + self.deliver_pinned(&source, parts, &record).await + } + KeptOutcome::Declined => self.deliver_fresh(services, origin, parts).await, + }; + } + // Only a reader's own `GET` of a version the origin serves in byte + // ranges starts the work of hashing it. + if self.reader_method == Method::HEAD { + log::debug!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=head_starts_no_load" + ); + return response; + } + if !advertises_byte_ranges(response.headers()) { + log::info!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=origin_ranges_not_advertised" + ); + return response; + } + if budget.remaining().is_zero() { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=timeout" + ); + if looked_up.elapsed() <= ORIGIN_BODY_MAX_WAIT { + return response; + } + let (parts, _) = response.into_parts(); + return self.deliver_fresh(services, origin, parts).await; + } + // The origin's own body is not read: the load reads the version + // pinned, so it serves every caller waiting on it. + let (parts, _) = response.into_parts(); + let load = LargeLoad { + signer: Arc::clone(&self.signer), + services: services.clone().without_connection(), + url: origin.url.clone(), + headers: origin.headers.clone(), + backend_name: origin.backend_name.to_owned(), + auth: origin.auth.cloned(), + unsigned_headers: origin.unsigned_headers.to_vec(), + version: version.clone(), + reader_url: identity.reader_url.to_owned(), + content_type: header_text(&parts.headers, &header::CONTENT_TYPE).to_owned(), + references, + namespace, + budget, + keep_for, + }; + let loaded = match futures::future::select( + Box::pin(services.load_cached(&self.spec, &key, load.into_loader())), + timer.sleep(budget.remaining()), + ) + .await + { + Either::Left((loaded, _)) => loaded.ok().and_then(|bytes| decode_kept_outcome(&bytes)), + Either::Right(((), _)) => { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=timeout" + ); + None + } + }; + match loaded { + Some(KeptOutcome::Signed(record)) if record.length == version.length() => { + let source = VersionedMediaSource::pinned(services, origin, version); + self.deliver_pinned(&source, parts, &record).await + } + _ => self.deliver_fresh(services, origin, parts).await, + } + } + + /// Answers the reader of a version the lane does not sign with one + /// fresh origin answer to the request as core first sent it, the + /// reader's own range and conditions included, its origin signing + /// renewed. Nothing is retried. + /// + /// The status, body and every header describing the representation + /// are the fresh answer's, so a changed version goes with its own + /// validators. The rest are `parts`, the headers core and its caller + /// prepared for this reader, to which the fresh answer's `Link` and + /// `Content-Security-Policy` values are added, never replacing the + /// caller's: provenance the origin now declares is kept, and several + /// policies only narrow what a reader allows. The caller's cache policy + /// describes the first answer, not this one, so the answer is held by + /// no cache at all. A cookie or any other header the fresh answer + /// sets is never passed on, since the caller's own handling never saw + /// it. + async fn deliver_fresh( + &self, + services: &RuntimeServices, + origin: PinnedOrigin<'_>, + mut parts: http::response::Parts, + ) -> Response { + let signer_id = self.signer.signer_id(); + let Ok(mut request) = origin.signed_request(&self.reader_method, origin.base_headers()) + else { + log::warn!( + "media signing: outcome=unanswered lane=large signer={signer_id} reason=origin_request_unsigned" + ); + return origin_unreachable(); + }; + if services.http_client().supports_streaming_responses() { + request = request.with_stream_response(); + } + // The platform's error may name the address, which can carry a + // credential, so it is not logged. + let Ok(fresh) = services.http_client().send(request).await else { + log::warn!( + "media signing: outcome=unanswered lane=large signer={signer_id} reason=origin_unreachable" + ); + return origin_unreachable(); + }; + let (mut fresh, body) = fresh.response.into_parts(); + // Fresh cookies are not delivered. The existing redirect-loop guard + // must not assume they will change the reader's next request. + fresh.headers.remove(header::SET_COOKIE); + if let Some(rewrite) = origin.response_header_rewrite { + crate::response_header_rewrite::rewrite_origin_urls_in_headers( + &mut fresh.headers, + &rewrite, + ); + } + parts.status = fresh.status; + for name in FRESH_REPRESENTATION_HEADERS { + parts.headers.remove(name); + for value in fresh.headers.get_all(name) { + parts.headers.append(name, value.clone()); + } + } + for name in [header::LINK, header::CONTENT_SECURITY_POLICY] { + for value in fresh.headers.get_all(&name) { + if !parts + .headers + .get_all(&name) + .iter() + .any(|kept| kept == value) + { + parts.headers.append(name.clone(), value.clone()); + } + } + } + let mut response = Response::from_parts(parts, body); + crate::cache_policy::remove_owned_cache_headers(&mut response); + let headers = response.headers_mut(); + crate::cache_policy::apply_no_store_private_to_headers(headers); + for name in [header::EXPIRES, header::AGE] { + headers.remove(name); + } + response + } + + /// Decorates the origin's `304` for a large version kept as signed + /// under the signer's current namespace, so a reader revalidating it is + /// told of its manifest. Any other `304` goes as it came. + pub(super) async fn decorate_not_modified( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + facts: &OriginFacts, + response: Response, + ) -> Response { + // A `304` whose own `Link` already names a manifest keeps the + // origin's provenance as it came; no kept outcome is added to it. + if !self.large_lane_open() + || !self.looks_like_media + || facts.private + || carries_manifest_link(response.headers()) + { + return response; + } + if self.signer.lookup_only() { + let Some(version) = SourceVersion::of(response.headers(), 0) else { + return response; + }; + let Some(origin_host) = url::Url::parse(identity.fetched_url) + .ok() + .and_then(|url| url.host_str().map(str::to_ascii_lowercase)) + else { + return response; + }; + let Some(record) = self + .published_large(services, identity, &origin_host, &response, &version, None) + .await + else { + return response; + }; + let (mut parts, body) = response.into_parts(); + decorate(&mut parts.headers, &record); + return Response::from_parts(parts, body); + } + let Some(version) = SourceVersion::of(response.headers(), 0) else { + return response; + }; + let Some(timer) = services.timer() else { + return response; + }; + let budget = LargeBudget { + started: Instant::now(), + total: self.signer.max_large_media_duration(), + }; + let remaining = budget.remaining(); + if remaining.is_zero() { + return response; + } + let namespace = + match futures::future::select(self.signer.namespace(services), timer.sleep(remaining)) + .await + { + Either::Left((Ok(namespace), _)) => namespace, + _ => return response, + }; + let key = self.large_store_key(services, &namespace, identity, version.etag()); + let remaining = budget.remaining(); + if remaining.is_zero() { + return response; + } + let record = match futures::future::select( + Box::pin(self.large_kept(services, &key, None)), + timer.sleep(remaining), + ) + .await + { + Either::Left((Some(KeptOutcome::Signed(record)), _)) => record, + _ => return response, + }; + let (mut parts, body) = response.into_parts(); + decorate(&mut parts.headers, &record); + Response::from_parts(parts, body) + } + + /// Answers the reader from an origin answer admitted to the large lane + /// for a lookup-only signer: the origin's own answer, decorated with + /// the version's published headers where the signer holds a result for + /// exactly this version and length, else as it came. Nothing is hashed, + /// signed, kept or loaded, and no other caller's work is joined. + async fn deliver_published_large( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + origin: PinnedOrigin<'_>, + answer: LargeAnswer, + ) -> Response { + let LargeAnswer { + response, version, .. + } = answer; + let origin_host = origin.url.host_str().map(str::to_ascii_lowercase); + let Some(origin_host) = origin_host else { + return response; + }; + let Some(record) = self + .published_large( + services, + identity, + &origin_host, + &response, + &version, + Some(version.length()), + ) + .await + else { + return response; + }; + let (mut parts, body) = response.into_parts(); + decorate(&mut parts.headers, &record); + Response::from_parts(parts, body) + } + + /// The lookup-only signer's published record for `version`, checked + /// against `length` where the answer states it. + async fn published_large( + &self, + services: &RuntimeServices, + identity: &VariantIdentity<'_>, + origin_host: &str, + response: &Response, + version: &SourceVersion, + length: Option, + ) -> Option { + let job = super::LargeLookupJob { + reader_url: identity.reader_url, + reader_query: identity.reader_query, + origin_identity: identity.origin_identity, + source_origin_host: origin_host, + content_type: header_text(response.headers(), &header::CONTENT_TYPE), + etag: version.etag(), + length: length.unwrap_or(0), + }; + let signer_id = self.signer.signer_id(); + match self.signer.lookup_large(services, &job).await { + Ok(published) + if length.is_none_or(|length| published.length == length) + && published.headers.len() <= LARGE_MAX_HEADERS + && !published + .headers + .iter() + .any(|(name, _)| is_reserved_header(name)) => + { + log::debug!("media signing: lane=large signer={signer_id} from=published"); + Some(LargeRecord { + length: published.length, + sha256: published.sha256, + headers: published.headers, + }) + } + Ok(_) => { + log::warn!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason=published_record_rejected" + ); + None + } + Err(failure) => { + log::info!( + "media signing: outcome=unsigned lane=large signer={signer_id} reason={failure}" + ); + None + } + } + } + + /// Answers the reader's preconditions and range against the pinned, + /// signed version, reading the bytes it needs by pinned reads, on + /// `parts`, the origin's headers as core prepared them, and adds the + /// signed version's headers from `record`. A version the origin no + /// longer serves is refused, never replaced, since the headers bind + /// these exact bytes. + async fn deliver_pinned( + &self, + source: &VersionedMediaSource<'_>, + mut parts: http::response::Parts, + record: &LargeRecord, + ) -> Response { + let length = source.size(); + let outcome = evaluate( + &self.reader_method, + &self.preconditions, + Some(source.version().etag()), + None, + length, + ); + let carries_body = self.reader_method != Method::HEAD; + let range = match outcome { + Outcome::Full if carries_body => length.checked_sub(1).map(|last| (0, last)), + Outcome::Partial { first, last } => Some((first, last)), + _ => None, + }; + let body = match range { + Some((first, last)) => { + let read = source.send_pinned(first, last).await; + match read.and_then(|body| exact_body(body, last - first + 1)) { + Ok(body) => body, + Err(error) => { + log::warn!( + "media signing: outcome=refused lane=large signer={} reason={}", + self.signer.signer_id(), + error.code() + ); + return refusal(); + } + } + } + None => EdgeBody::empty(), + }; + let headers = &mut parts.headers; + for name in [ + "content-range", + "content-length", + "transfer-encoding", + "content-encoding", + ] { + headers.remove(name); + } + match outcome { + Outcome::Full => { + parts.status = StatusCode::OK; + headers.insert(header::CONTENT_LENGTH, HeaderValue::from(length)); + } + Outcome::Partial { first, last } => { + parts.status = StatusCode::PARTIAL_CONTENT; + headers.insert( + header::CONTENT_RANGE, + HeaderValue::from_str(&format!("bytes {first}-{last}/{length}")) + .expect("should build a content range"), + ); + headers.insert(header::CONTENT_LENGTH, HeaderValue::from(last - first + 1)); + } + Outcome::NotModified => { + parts.status = StatusCode::NOT_MODIFIED; + } + Outcome::PreconditionFailed => { + parts.status = StatusCode::PRECONDITION_FAILED; + headers.remove(header::CONTENT_TYPE); + headers.insert(header::CONTENT_LENGTH, HeaderValue::from(0_u64)); + } + Outcome::Unsatisfiable => { + parts.status = StatusCode::RANGE_NOT_SATISFIABLE; + headers.insert( + header::CONTENT_RANGE, + HeaderValue::from_str(&format!("bytes */{length}")) + .expect("should build a content range"), + ); + headers.remove(header::CONTENT_TYPE); + headers.insert(header::CONTENT_LENGTH, HeaderValue::from(0_u64)); + } + } + if matches!( + outcome, + Outcome::Full | Outcome::Partial { .. } | Outcome::NotModified + ) { + decorate(headers, record); + } + Response::from_parts(parts, body) + } + + /// The outcome kept under `key`, or `None`. A signed outcome whose + /// length is not `length`, where an answer states one, or a record that + /// does not read back, is dropped. The lookup itself runs no load, but + /// waits on one another caller is running for `key`. + async fn large_kept( + &self, + services: &RuntimeServices, + key: &StoreKey, + length: Option, + ) -> Option { + if !services.loading_cache_for(ASSETS_CACHE).keeps_values() { + return None; + } + let bytes = services + .load_cached( + &self.spec, + key, + Box::new(|| Box::pin(async { Err(LoadError::new(super::NOT_KEPT)) })), + ) + .await + .ok()?; + let kept = decode_kept_outcome(&bytes).filter(|kept| match kept { + KeptOutcome::Signed(record) => length.is_none_or(|length| record.length == length), + KeptOutcome::Declined => true, + }); + if kept.is_none() { + log::warn!( + "media signing: lane=large kept outcome unreadable or for another length signer={}", + self.signer.signer_id() + ); + services + .loading_cache_for(ASSETS_CACHE) + .remove(&services.clone().without_connection(), &self.spec, key) + .await; + } + kept + } + + /// The key the outcome for `identity`'s version `etag` is kept under, + /// bound to the publisher's prefix, the signer and its `namespace`. + fn large_store_key( + &self, + services: &RuntimeServices, + namespace: &str, + identity: &VariantIdentity<'_>, + etag: &str, + ) -> StoreKey { + let mut digest = Sha256::new(); + for part in [ + self.signer.signer_id(), + namespace, + identity.reader_url, + identity.origin_identity, + identity.fetched_url, + etag, + ] { + digest.update(part.as_bytes()); + digest.update([0]); + } + StoreKey::new( + services.server_key_prefix(), + ASSETS_CACHE, + format!("media-large-{}", hex::encode(digest.finalize())), + ) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/trusted-server-core/src/media_signing/large/tests.rs b/crates/trusted-server-core/src/media_signing/large/tests.rs new file mode 100644 index 0000000000..a1c0c0cda4 --- /dev/null +++ b/crates/trusted-server-core/src/media_signing/large/tests.rs @@ -0,0 +1,2017 @@ +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; + +use error_stack::Report; +use futures::FutureExt as _; +use futures::channel::oneshot; +use futures::executor::block_on; + +use super::*; +use crate::error::TrustedServerError; +use crate::media_signing::test_support::MANIFEST_LINK; +use crate::media_signing::{Answered, MediaJob, MediaNamespacing, MediaSigning}; +use crate::origin_auth::OriginSigner; +use crate::platform::test_support::{ + ElapsedTimer, MemoryLoadingCache, StoppedTimer, build_services_with_http_client, +}; +use crate::platform::{ + CacheSpec, PlatformError, PlatformHttpClient, PlatformLoadingCache, PlatformPendingRequest, + PlatformResponse, PlatformSelectResult, PlatformTimer, +}; +use crate::test_support::tests::create_test_settings; + +const PATH: &str = "/media/42"; +const FETCHED_URL: &str = "https://origin.example.com/media/42"; +const QUERY_FETCHED_URL: &str = "https://origin.example.com/media/42?v=2"; +const READER_URL: &str = "https://www.example.com/media/42"; +const ORIGIN_IDENTITY: &str = "https://origin.example.com\0origin.example.com"; +const ETAG_V1: &str = "\"v1\""; +const ETAG_V2: &str = "\"v2\""; +/// The size of each chunk the stub origin streams, so every body arrives +/// in several pieces. +const CHUNK: usize = 5; +/// The `Authorization` the test origin signer writes. +const ORIGIN_SIGNATURE: &str = "TestOrigin signed"; + +fn source() -> Vec { + (0_u8..64).collect() +} + +fn other_source() -> Vec { + (100_u8..164).collect() +} + +fn identity() -> VariantIdentity<'static> { + VariantIdentity { + reader_url: READER_URL, + origin_identity: ORIGIN_IDENTITY, + fetched_url: FETCHED_URL, + reader_query: "", + } +} + +/// An origin answer for `bytes` at version `etag`: the bytes `range` +/// names as a `206`, or all of them as a `200`, streamed in [`CHUNK`]s and +/// cut with a stream error after `cut_at` bytes where given. +fn serve( + bytes: &[u8], + etag: &str, + range: Option<(u64, u64)>, + cut_at: Option, +) -> Response { + let total = bytes.len(); + let (status, slice) = match range { + Some((first, last)) => { + let first = usize::try_from(first).expect("should fit a test offset"); + let last = usize::try_from(last).expect("should fit a test offset"); + (StatusCode::PARTIAL_CONTENT, bytes[first..=last].to_vec()) + } + None => (StatusCode::OK, bytes.to_vec()), + }; + let mut builder = Response::builder() + .status(status) + .header(header::CONTENT_TYPE, "image/png") + .header(header::ETAG, etag) + .header(header::CACHE_CONTROL, "public, max-age=600") + .header(header::ACCEPT_RANGES, "bytes") + .header(header::CONTENT_LENGTH, slice.len()); + if let Some((first, last)) = range { + builder = builder.header( + header::CONTENT_RANGE, + format!("bytes {first}-{last}/{total}"), + ); + } + let kept = cut_at.map_or(slice.len(), |cut| cut.min(slice.len())); + let mut items: Vec> = slice[..kept] + .chunks(CHUNK) + .map(|chunk| Ok(Bytes::copy_from_slice(chunk))) + .collect(); + if cut_at.is_some() { + items.push(Err(std::io::Error::other("origin connection cut"))); + } + builder + .body(EdgeBody::from_stream(futures::stream::iter(items))) + .expect("should build an origin answer") +} + +/// The origin's whole `200` for the reader's first request. +fn whole() -> Response { + serve(&source(), ETAG_V1, None, None) +} + +/// The origin's answer to a `HEAD`: the whole's headers, no body. +fn head_answer(bytes: &[u8], etag: &str) -> Response { + let (parts, _) = serve(bytes, etag, None, None).into_parts(); + Response::from_parts(parts, EdgeBody::empty()) +} + +fn not_modified(etag: &str) -> Response { + Response::builder() + .status(StatusCode::NOT_MODIFIED) + .header(header::ETAG, etag) + .header(header::CACHE_CONTROL, "public, max-age=600") + .body(EdgeBody::empty()) + .expect("should build a not modified answer") +} + +fn assert_unchanged_not_modified(delivery: Delivery, headers: &HeaderMap) { + let Delivery::Send(response) = delivery else { + panic!("should send the original not modified response"); + }; + assert_eq!(response.status(), StatusCode::NOT_MODIFIED); + assert_eq!(response.headers(), headers, "should preserve every header"); + let (_, body) = collect(response); + assert_eq!(body, Some(Vec::new()), "should preserve the empty body"); +} + +/// A `bytes=first-last` request range. +fn requested_range(headers: &HeaderMap) -> Option<(u64, u64)> { + let value = headers.get(header::RANGE)?.to_str().ok()?; + let (first, last) = value.strip_prefix("bytes=")?.split_once('-')?; + Some((first.parse().ok()?, last.parse().ok()?)) +} + +struct VersionChange { + after: usize, + bytes: Vec, + etag: String, + headers: Vec<(HeaderName, HeaderValue)>, +} + +#[derive(Default)] +struct OriginState { + bytes: Vec, + etag: String, + /// From the request after this many on, the origin serves the other + /// version, answering it with these headers too. + flip: Option, + /// Headers every answer carries besides [`serve`]'s own, replacing + /// them. + answer_headers: Vec<(HeaderName, HeaderValue)>, + /// Override the status after this many requests. + status_after: Option<(usize, StatusCode)>, + /// The next answer's stream is cut after this many bytes. + cut_next: Option, + sends: usize, + wrong_content_range: bool, + requests: Vec, +} + +/// An origin that honors `Range` and `If-Match` and can change version. +struct RangeOrigin { + state: Mutex, +} + +impl RangeOrigin { + fn new(bytes: &[u8], etag: &str) -> Arc { + Arc::new(Self { + state: Mutex::new(OriginState { + bytes: bytes.to_vec(), + etag: etag.to_owned(), + ..OriginState::default() + }), + }) + } + + /// Serves `bytes` at `etag` from the request after the `after`th on. + fn flip_after(&self, after: usize, bytes: &[u8], etag: &str) { + self.flip_after_with(after, bytes, etag, Vec::new()); + } + + /// As [`Self::flip_after`], the new version's answers carrying + /// `headers` too. + fn flip_after_with( + &self, + after: usize, + bytes: &[u8], + etag: &str, + headers: Vec<(HeaderName, HeaderValue)>, + ) { + self.state.lock().expect("should lock origin").flip = Some(VersionChange { + after, + bytes: bytes.to_vec(), + etag: etag.to_owned(), + headers, + }); + } + + fn cut_next_at(&self, at: usize) { + self.state.lock().expect("should lock origin").cut_next = Some(at); + } + + fn answer_wrong_content_range(&self) { + self.state + .lock() + .expect("should lock origin") + .wrong_content_range = true; + } + + fn requests(&self) -> Vec { + self.state + .lock() + .expect("should lock origin") + .requests + .clone() + } + + /// The ranges every request asked for, in order. + fn ranges(&self) -> Vec> { + self.requests().iter().map(requested_range).collect() + } + + /// How many requests read the whole version, which only hashing does + /// besides a whole delivery. + fn whole_reads(&self) -> usize { + self.ranges() + .iter() + .filter(|range| **range == Some((0, 63))) + .count() + } +} + +#[async_trait::async_trait(?Send)] +impl PlatformHttpClient for RangeOrigin { + fn supports_streaming_responses(&self) -> bool { + true + } + + async fn send( + &self, + request: PlatformHttpRequest, + ) -> Result> { + let mut state = self.state.lock().expect("should lock origin"); + if state + .flip + .as_ref() + .is_some_and(|flip| state.sends >= flip.after) + { + let flip = state.flip.take().expect("should have a due version change"); + state.bytes = flip.bytes; + state.etag = flip.etag; + state.answer_headers = flip.headers; + } + state.sends += 1; + let headers = request.request.headers().clone(); + state.requests.push(headers.clone()); + if let Some(if_match) = headers.get(header::IF_MATCH) + && if_match.as_bytes() != state.etag.as_bytes() + { + let refused = Response::builder() + .status(StatusCode::PRECONDITION_FAILED) + .header(header::ETAG, state.etag.as_str()) + .body(EdgeBody::empty()) + .expect("should build a refusal"); + return Ok(PlatformResponse::new(refused)); + } + let range = requested_range(&headers); + let cut = state.cut_next.take(); + let mut response = serve(&state.bytes, &state.etag, range, cut); + if let Some((after, status)) = state.status_after + && state.sends > after + { + *response.status_mut() = status; + } + for (name, value) in &state.answer_headers { + response.headers_mut().insert(name.clone(), value.clone()); + } + if state.wrong_content_range && range.is_some() { + response.headers_mut().insert( + header::CONTENT_RANGE, + HeaderValue::from_static("bytes 0-0/1"), + ); + } + Ok(PlatformResponse::new(response)) + } + + async fn send_async( + &self, + _request: PlatformHttpRequest, + ) -> Result> { + Err(Report::new(PlatformError::HttpClient)) + } + + async fn select( + &self, + _pending_requests: Vec, + ) -> Result> { + Err(Report::new(PlatformError::HttpClient)) + } +} + +/// An origin signer that writes [`ORIGIN_SIGNATURE`] and owns every +/// `x-test-origin-*` header. +struct TestOriginSigner; + +impl OriginSigner for TestOriginSigner { + fn sign( + &self, + _method: &Method, + _url: &url::Url, + headers: &mut HeaderMap, + _now: SystemTime, + ) -> Result<(), Report> { + headers.insert( + header::AUTHORIZATION, + HeaderValue::from_static(ORIGIN_SIGNATURE), + ); + Ok(()) + } + + fn remove_namespace_headers(&self, headers: &mut HeaderMap) { + headers.remove("x-test-origin-key"); + } +} + +/// One large job the stub signer was handed. +#[derive(Debug, Clone)] +struct LargeJobRecord { + source_sha256: String, + size: u64, + remaining: Duration, + source_reference: Option, + reader_reference: Option, + origin_host: String, +} + +/// How the stub signer answers a large job. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LargeBehavior { + /// Signs, adding [`MANIFEST_LINK`]. + Sign, + /// Signs after confirming the pinned version is still current. + ConfirmThenSign, + /// Refuses, as a source with provenance of its own is refused. + Refuse, + /// Never answers. + Hang, +} + +/// A signer whose bounded limit is 16 bytes and whose large lane takes up +/// to 1 KiB. +struct LargeSigner { + behavior: LargeBehavior, + namespace: Mutex>, + namespace_gate: Mutex>>, + large_duration: Duration, + attempts: Mutex, + jobs: Mutex>, + failures: Mutex>, +} + +impl LargeSigner { + fn new(behavior: LargeBehavior) -> Arc { + Arc::new(Self { + behavior, + namespace: Mutex::new(Ok("policy-1".to_owned())), + namespace_gate: Mutex::new(None), + large_duration: Duration::from_secs(60), + attempts: Mutex::new(0), + jobs: Mutex::new(Vec::new()), + failures: Mutex::new(Vec::new()), + }) + } + + fn set_namespace(&self, namespace: Result) { + *self.namespace.lock().expect("should lock namespace") = namespace; + } + + /// How many times core called `sign_large`. + fn attempts(&self) -> usize { + *self.attempts.lock().expect("should lock attempts") + } + + fn jobs(&self) -> Vec { + self.jobs.lock().expect("should lock jobs").clone() + } + + fn failures(&self) -> Vec<&'static str> { + self.failures.lock().expect("should lock failures").clone() + } +} + +impl MediaSigner for LargeSigner { + fn signer_id(&self) -> &'static str { + "testing.large-media" + } + + fn handles_path(&self, path: &str) -> bool { + !path.starts_with("/api/") + } + + fn handles_media_type(&self, media_type: &str) -> bool { + media_type.starts_with("image/") + } + + fn max_source_bytes(&self) -> usize { + 16 + } + + fn namespace<'a>(&'a self, _services: &'a RuntimeServices) -> MediaNamespacing<'a> { + let namespace = self + .namespace + .lock() + .expect("should lock namespace") + .clone(); + let gate = self + .namespace_gate + .lock() + .expect("should lock namespace gate") + .take(); + Box::pin(async move { + if let Some(gate) = gate { + gate.await.expect("should release namespace"); + } + namespace + }) + } + + fn sign<'a>( + &'a self, + _services: &'a RuntimeServices, + _job: &'a MediaJob<'a>, + ) -> MediaSigning<'a> { + Box::pin(async { + Err(MediaSigningFailure { + reason: "bounded_not_expected", + }) + }) + } + + fn max_large_source_bytes(&self) -> u64 { + 1024 + } + + fn max_large_media_duration(&self) -> Duration { + self.large_duration + } + + fn sign_large<'a>( + &'a self, + _services: &'a RuntimeServices, + job: &'a LargeMediaJob<'a>, + ) -> LargeMediaSigning<'a> { + Box::pin(async move { + *self.attempts.lock().expect("should lock attempts") += 1; + match self.behavior { + LargeBehavior::Refuse => { + return Err(MediaSigningFailure { + reason: "source_has_embedded_provenance", + }); + } + LargeBehavior::Hang => futures::future::pending::<()>().await, + LargeBehavior::ConfirmThenSign => { + if let Err(error) = job.source.confirm_current().await { + self.failures + .lock() + .expect("should lock failures") + .push(error.code()); + return Err(MediaSigningFailure { + reason: error.code(), + }); + } + } + LargeBehavior::Sign => {} + } + self.jobs + .lock() + .expect("should lock jobs") + .push(LargeJobRecord { + source_sha256: job.source_sha256.to_owned(), + size: job.source.size(), + remaining: job.remaining, + source_reference: job.source_reference.map(str::to_owned), + reader_reference: job.reader_reference.map(str::to_owned), + origin_host: job.source_origin_host.to_owned(), + }); + let namespace = self + .namespace + .lock() + .expect("should lock namespace") + .clone() + .unwrap_or_default(); + Ok(LargeSignedMedia { + headers: vec![(header::LINK, HeaderValue::from_static(MANIFEST_LINK))], + namespace, + }) + }) + } +} + +#[derive(Default)] +struct PendingLargeCache { + calls: Mutex, +} + +#[async_trait::async_trait(?Send)] +impl PlatformLoadingCache for PendingLargeCache { + async fn get_or_load( + &self, + _services: &RuntimeServices, + _spec: &CacheSpec, + _key: &StoreKey, + _load: Loader, + ) -> Result { + *self.calls.lock().expect("should lock cache calls") += 1; + futures::future::pending().await + } + + async fn remove(&self, _services: &RuntimeServices, _spec: &CacheSpec, _key: &StoreKey) { + panic!("should never remove a pending cache outcome"); + } +} + +fn services_with_timer( + signer: &Arc, + origin: &Arc, + timer: Arc, +) -> RuntimeServices { + build_services_with_http_client(Arc::clone(origin) as Arc) + .with_media_signer(Some(Arc::clone(signer) as Arc)) + .with_loading_cache(Arc::new(MemoryLoadingCache::default())) + .with_timer(timer) +} + +fn services_for(signer: &Arc, origin: &Arc) -> RuntimeServices { + services_with_timer(signer, origin, Arc::new(StoppedTimer)) +} + +fn claim_for( + services: &RuntimeServices, + method: &Method, + pairs: &[(HeaderName, &str)], +) -> MediaClaim { + let mut headers = HeaderMap::new(); + headers.insert("sec-fetch-dest", HeaderValue::from_static("image")); + for (name, value) in pairs { + headers.insert( + name.clone(), + HeaderValue::from_str(value).expect("should build a header"), + ); + } + MediaClaim::of_request( + &create_test_settings(), + services, + method, + PATH, + &headers, + true, + ) + .expect("should claim a media path") +} + +/// How core asks the origin again in a test: the address, the request's +/// headers before signing, and the origin's signing where it has one. +struct TestOrigin<'a> { + identity: VariantIdentity<'a>, + headers: HeaderMap, + auth: Option, + unsigned_headers: Vec, + response_header_rewrite: Option>, +} + +impl TestOrigin<'static> { + /// An origin request that forwarded the reader's `pairs`, as a route + /// forwards a reader's range and conditions. + fn forwarding(pairs: &[(HeaderName, &str)]) -> Self { + let mut headers = HeaderMap::new(); + for (name, value) in pairs { + headers.insert( + name.clone(), + HeaderValue::from_str(value).expect("should build a header"), + ); + } + Self { + headers, + ..Self::plain() + } + } + + fn plain() -> Self { + Self { + identity: identity(), + headers: HeaderMap::new(), + auth: None, + unsigned_headers: Vec::new(), + response_header_rewrite: None, + } + } +} + +/// Delivers the origin's `response` through `origin`, by the large lane +/// where core admits it. Whether it was admitted, and the answer. +fn run_through( + claim: &MediaClaim, + services: &RuntimeServices, + origin: &TestOrigin<'_>, + response: Response, +) -> (bool, Response) { + block_on(async { + let facts = OriginFacts::of(response.headers()); + match claim + .deliver( + services, + &origin.identity, + Answered::Origin(facts), + response, + ) + .await + { + Delivery::Large(answer) => { + let url = url::Url::parse(origin.identity.fetched_url) + .expect("should parse the fetched address"); + let pinned = PinnedOrigin { + url: &url, + headers: &origin.headers, + backend_name: "origin", + auth: origin.auth.as_ref(), + unsigned_headers: &origin.unsigned_headers, + response_header_rewrite: origin.response_header_rewrite, + }; + ( + true, + claim + .deliver_large(services, &origin.identity, pinned, answer) + .await, + ) + } + Delivery::Send(response) => (false, response), + Delivery::FetchWhole(_) => { + panic!("should never ask for a bounded whole of a large source") + } + } + }) +} + +fn run( + claim: &MediaClaim, + services: &RuntimeServices, + response: Response, +) -> (bool, Response) { + run_through(claim, services, &TestOrigin::plain(), response) +} + +/// The body's bytes, or `None` where its stream failed. +fn collect(response: Response) -> (Response, Option>) { + let (parts, body) = response.into_parts(); + let bytes = block_on(async { + match body { + EdgeBody::Once(bytes) => Some(bytes.to_vec()), + EdgeBody::Stream(mut stream) => { + let mut collected = Vec::new(); + while let Some(chunk) = stream.next().await { + collected.extend_from_slice(&chunk.ok()?); + } + Some(collected) + } + } + }); + (Response::from_parts(parts, EdgeBody::empty()), bytes) +} + +fn header_of<'a>(response: &'a Response, name: &str) -> Option<&'a str> { + response + .headers() + .get(name) + .and_then(|value| value.to_str().ok()) +} + +fn whole_digest_header(bytes: &[u8]) -> String { + format!( + "sha-256=:{}:", + base64::engine::general_purpose::STANDARD.encode(Sha256::digest(bytes)) + ) +} + +#[test] +fn hashes_a_large_source_once_and_delivers_the_exact_pinned_bytes() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::ConfirmThenSign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + + let (large, response) = run(&claim, &services, whole()); + let (response, body) = collect(response); + + assert!( + large, + "should admit a source past the bounded limit with a strong tag" + ); + let jobs = signer.jobs(); + assert_eq!(jobs.len(), 1, "should sign once"); + assert_eq!( + jobs[0].source_sha256, + hex::encode(Sha256::digest(source())), + "should hand the signer the digest of every source byte" + ); + assert_eq!(jobs[0].size, 64, "should pin the declared length"); + assert!( + jobs[0].remaining > Duration::ZERO && jobs[0].remaining <= Duration::from_secs(60), + "should tell the signer what is left of its budget" + ); + assert_eq!(jobs[0].source_reference.as_deref(), Some(FETCHED_URL)); + assert_eq!(jobs[0].reader_reference.as_deref(), Some(READER_URL)); + assert_eq!(jobs[0].origin_host, "origin.example.com"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + body, + Some(source()), + "should deliver the origin's exact bytes" + ); + assert_eq!( + header_of(&response, "etag"), + Some(ETAG_V1), + "should keep the origin's validator" + ); + assert_eq!(header_of(&response, "content-length"), Some("64")); + assert_eq!( + header_of(&response, "link"), + Some(MANIFEST_LINK), + "should carry the manifest link" + ); + assert_eq!( + header_of(&response, "repr-digest"), + Some(whole_digest_header(&source()).as_str()), + "should describe the whole representation" + ); + let requests = origin.requests(); + for request in &requests { + assert_eq!( + request + .get(header::IF_MATCH) + .and_then(|value| value.to_str().ok()), + Some(ETAG_V1), + "should pin every origin read to the version" + ); + assert!( + request.get(header::RANGE).is_some(), + "should ask for exact ranges" + ); + } + assert_eq!( + origin.ranges(), + vec![Some((0, 63)), Some((0, 0)), Some((0, 63))], + "should hash once, confirm, then deliver every byte of the pinned version" + ); +} + +#[test] +fn answers_a_reader_range_with_the_exact_original_bytes() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::ConfirmThenSign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[(header::RANGE, "bytes=10-19")]); + + let (large, response) = run( + &claim, + &services, + serve(&source(), ETAG_V1, Some((10, 19)), None), + ); + let (response, body) = collect(response); + + assert!( + large, + "should admit a part whose total is past the bounded limit" + ); + assert_eq!( + signer.jobs()[0].source_sha256, + hex::encode(Sha256::digest(source())), + "should hash the whole version, not the part" + ); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!( + header_of(&response, "content-range"), + Some("bytes 10-19/64") + ); + assert_eq!(header_of(&response, "content-length"), Some("10")); + assert_eq!( + body, + Some(source()[10..20].to_vec()), + "should send the original bytes of the range" + ); + assert_eq!(header_of(&response, "link"), Some(MANIFEST_LINK)); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + assert_eq!( + origin.ranges(), + vec![Some((0, 63)), Some((0, 0)), Some((10, 19))], + "should hash the whole pinned version once, then read the reader's range pinned" + ); + + let (_, response) = run( + &claim, + &services, + serve(&source(), ETAG_V1, Some((10, 19)), None), + ); + let (response, body) = collect(response); + assert_eq!( + origin.whole_reads(), + 1, + "a second range should never hash again" + ); + assert_eq!(body, Some(source()[10..20].to_vec())); + assert_eq!(header_of(&response, "link"), Some(MANIFEST_LINK)); +} + +#[test] +fn a_cold_head_starts_no_load_and_a_warm_one_is_decorated() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let head = claim_for(&services, &Method::HEAD, &[]); + + let (large, response) = run(&head, &services, head_answer(&source(), ETAG_V1)); + let (response, body) = collect(response); + assert!(large, "should admit a HEAD of a large source"); + assert!( + origin.requests().is_empty(), + "a cold HEAD should never read the origin again" + ); + assert_eq!( + signer.attempts(), + 0, + "a cold HEAD should never call the signer" + ); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(header_of(&response, "content-length"), Some("64")); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + assert_eq!(header_of(&response, "link"), None); + assert_eq!(body, Some(Vec::new()), "a HEAD carries no body"); + + let get = claim_for(&services, &Method::GET, &[]); + let _ = collect(run(&get, &services, whole()).1); + let reads = origin.requests().len(); + let (_, response) = run(&head, &services, head_answer(&source(), ETAG_V1)); + let (response, body) = collect(response); + assert_eq!( + header_of(&response, "link"), + Some(MANIFEST_LINK), + "a warm HEAD carries the manifest" + ); + assert_eq!( + header_of(&response, "repr-digest"), + Some(whole_digest_header(&source()).as_str()) + ); + assert_eq!(header_of(&response, "content-length"), Some("64")); + assert_eq!(body, Some(Vec::new())); + assert_eq!( + origin.requests().len(), + reads, + "a warm HEAD should read nothing" + ); + assert_eq!(signer.attempts(), 1); +} + +#[test] +fn only_an_origin_that_says_it_serves_byte_ranges_starts_a_load() { + for accept_ranges in [None, Some("none")] { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let mut answer = whole(); + answer.headers_mut().remove(header::ACCEPT_RANGES); + if let Some(value) = accept_ranges { + answer + .headers_mut() + .insert(header::ACCEPT_RANGES, HeaderValue::from_static(value)); + } + + let (_, response) = run(&claim, &services, answer); + let (response, body) = collect(response); + + assert!( + origin.requests().is_empty(), + "{accept_ranges:?}: should never hash" + ); + assert_eq!(signer.attempts(), 0, "{accept_ranges:?}"); + assert_eq!( + body, + Some(source()), + "{accept_ranges:?}: should send the origin's answer" + ); + assert_eq!( + header_of(&response, "etag"), + Some(ETAG_V1), + "{accept_ranges:?}" + ); + assert_eq!(header_of(&response, "link"), None, "{accept_ranges:?}"); + } + + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let _ = collect(run(&claim, &services, whole()).1); + let mut answer = whole(); + answer.headers_mut().remove(header::ACCEPT_RANGES); + let (_, response) = run(&claim, &services, answer); + let (response, body) = collect(response); + assert_eq!(body, Some(source())); + assert_eq!( + header_of(&response, "link"), + Some(MANIFEST_LINK), + "a kept signed outcome still decorates the version" + ); + assert_eq!(signer.attempts(), 1); +} + +#[test] +fn a_kept_outcome_answers_without_hashing_and_never_outlives_the_policy() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let _ = collect(run(&claim, &services, whole()).1); + let reads = origin.requests().len(); + + let (_, response) = run(&claim, &services, whole()); + let (response, body) = collect(response); + assert_eq!( + signer.attempts(), + 1, + "should reuse the outcome kept for the version" + ); + assert_eq!( + origin.requests().len(), + reads, + "should send the origin's own answer" + ); + assert_eq!(body, Some(source())); + assert_eq!(header_of(&response, "link"), Some(MANIFEST_LINK)); + + let (_, revalidated) = run(&claim, &services, not_modified(ETAG_V1)); + assert_eq!(revalidated.status(), StatusCode::NOT_MODIFIED); + assert_eq!( + header_of(&revalidated, "link"), + Some(MANIFEST_LINK), + "should tell a revalidating reader of the manifest" + ); + + signer.set_namespace(Ok("policy-2".to_owned())); + let _ = collect(run(&claim, &services, whole()).1); + assert_eq!( + signer.attempts(), + 2, + "a changed policy should never find the old outcome" + ); + + signer.set_namespace(Err(MediaSigningFailure { reason: "revoked" })); + let reads = origin.requests().len(); + let (_, response) = run(&claim, &services, whole()); + let (response, body) = collect(response); + assert_eq!(signer.attempts(), 2, "a revoked policy should sign nothing"); + assert_eq!(origin.requests().len(), reads, "should hash nothing"); + assert_eq!( + header_of(&response, "link"), + None, + "should carry no manifest" + ); + assert_eq!( + header_of(&response, "etag"), + Some(ETAG_V1), + "should keep the origin's validator on its unchanged bytes" + ); + assert_eq!(body, Some(source())); +} + +#[test] +fn a_refused_source_is_hashed_once_and_then_sent_as_it_came() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Refuse); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[(header::RANGE, "bytes=0-9")]); + let forwarding = TestOrigin::forwarding(&[(header::RANGE, "bytes=0-9")]); + + let (_, response) = run_through( + &claim, + &services, + &forwarding, + serve(&source(), ETAG_V1, Some((0, 9)), None), + ); + let (response, body) = collect(response); + assert_eq!(signer.attempts(), 1); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!(header_of(&response, "content-range"), Some("bytes 0-9/64")); + assert_eq!(body, Some(source()[0..10].to_vec())); + assert_eq!(header_of(&response, "link"), None); + assert_eq!( + header_of(&response, "etag"), + Some(ETAG_V1), + "should keep the origin's validator" + ); + assert_eq!( + header_of(&response, "cache-control"), + Some(crate::cache_policy::NO_STORE_PRIVATE_CACHE_CONTROL), + "a fresh fallback is held by no cache, even for the same version" + ); + let fresh = origin + .requests() + .last() + .cloned() + .expect("should ask the origin afresh"); + assert_eq!( + requested_range(&fresh), + Some((0, 9)), + "should ask afresh exactly as the reader asked" + ); + assert!( + fresh.get(header::IF_MATCH).is_none(), + "an unsigned answer is not pinned" + ); + let reads = origin.requests().len(); + + for _ in 0..3 { + let (_, response) = run_through( + &claim, + &services, + &forwarding, + serve(&source(), ETAG_V1, Some((0, 9)), None), + ); + let (response, body) = collect(response); + assert_eq!(body, Some(source()[0..10].to_vec())); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + assert_eq!(header_of(&response, "link"), None); + } + assert_eq!( + signer.attempts(), + 1, + "a kept refusal should never call the signer again" + ); + assert_eq!( + origin.requests().len(), + reads, + "a kept refusal should never read the origin again" + ); + assert_eq!(origin.whole_reads(), 1, "should hash once in all"); +} + +#[test] +fn a_declined_signed_origin_is_asked_afresh_with_a_fresh_signature() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Refuse); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[(header::RANGE, "bytes=0-9")]); + let mut headers = HeaderMap::new(); + headers.insert(header::RANGE, HeaderValue::from_static("bytes=0-9")); + headers.insert( + header::REFERER, + HeaderValue::from_static("https://www.example.com/page"), + ); + headers.insert("x-test-origin-key", HeaderValue::from_static("from-reader")); + let signed = TestOrigin { + headers, + auth: Some(OriginSigning::new(Arc::new(TestOriginSigner), None)), + unsigned_headers: vec![header::REFERER], + ..TestOrigin::plain() + }; + + let (_, response) = run_through( + &claim, + &services, + &signed, + serve(&source(), ETAG_V1, Some((0, 9)), None), + ); + let (response, body) = collect(response); + + assert_eq!(body, Some(source()[0..10].to_vec())); + assert_eq!(header_of(&response, "link"), None); + let fresh = origin + .requests() + .last() + .cloned() + .expect("should ask the origin afresh"); + assert_eq!( + fresh + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()), + Some(ORIGIN_SIGNATURE), + "should sign the fresh request afresh" + ); + assert_eq!(requested_range(&fresh), Some((0, 9))); + assert!(fresh.get(header::REFERER).is_none()); + assert!(fresh.get("x-test-origin-key").is_none()); + assert!(fresh.get(header::IF_MATCH).is_none()); +} + +#[test] +fn a_not_modified_pending_namespace_obeys_the_large_budget() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let mut signer = LargeSigner::new(LargeBehavior::Sign); + let (namespace_ready, namespace_gate) = oneshot::channel(); + let configured = Arc::get_mut(&mut signer).expect("should exclusively own the signer"); + configured.large_duration = Duration::from_secs(5); + *configured + .namespace_gate + .lock() + .expect("should lock namespace gate") = Some(namespace_gate); + let timer = Arc::new(ElapsedTimer::default()); + let cache = Arc::new(PendingLargeCache::default()); + let services = services_with_timer( + &signer, + &origin, + Arc::clone(&timer) as Arc, + ) + .with_loading_cache(Arc::clone(&cache) as Arc); + let claim = claim_for(&services, &Method::GET, &[]); + let response = not_modified(ETAG_V1); + let headers = response.headers().clone(); + let facts = OriginFacts::of(response.headers()); + + let delivery = claim + .deliver(&services, &identity(), Answered::Origin(facts), response) + .now_or_never() + .expect("should stop waiting for the namespace at the large deadline"); + + assert_unchanged_not_modified(delivery, &headers); + assert_eq!(*cache.calls.lock().expect("should lock cache calls"), 0); + assert!(origin.requests().is_empty()); + assert_eq!(signer.attempts(), 0); + let requested = timer.requested.lock().expect("should lock timer requests"); + assert_eq!(requested.len(), 1); + assert!(requested[0] > Duration::ZERO && requested[0] <= Duration::from_secs(5)); + drop(namespace_ready); +} + +#[test] +fn a_not_modified_pending_cache_obeys_the_large_budget() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let mut signer = LargeSigner::new(LargeBehavior::Sign); + Arc::get_mut(&mut signer) + .expect("should exclusively own the signer") + .large_duration = Duration::from_secs(5); + let timer = Arc::new(ElapsedTimer::default()); + let cache = Arc::new(PendingLargeCache::default()); + let services = services_with_timer( + &signer, + &origin, + Arc::clone(&timer) as Arc, + ) + .with_loading_cache(Arc::clone(&cache) as Arc); + let claim = claim_for(&services, &Method::GET, &[]); + let response = not_modified(ETAG_V1); + let headers = response.headers().clone(); + let facts = OriginFacts::of(response.headers()); + + let delivery = claim + .deliver(&services, &identity(), Answered::Origin(facts), response) + .now_or_never() + .expect("should stop waiting for the cached outcome at the large deadline"); + + assert_unchanged_not_modified(delivery, &headers); + assert_eq!(*cache.calls.lock().expect("should lock cache calls"), 1); + assert!(origin.requests().is_empty()); + assert_eq!(signer.attempts(), 0); + let requested = timer.requested.lock().expect("should lock timer requests"); + assert_eq!(requested.len(), 1); + assert!(requested[0] > Duration::ZERO && requested[0] <= Duration::from_secs(5)); +} + +#[test] +fn a_not_modified_without_a_timer_or_positive_large_budget_starts_no_io() { + for (has_timer, budget) in [(false, Duration::from_secs(5)), (true, Duration::ZERO)] { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let mut signer = LargeSigner::new(LargeBehavior::Sign); + let (namespace_ready, namespace_gate) = oneshot::channel(); + let configured = Arc::get_mut(&mut signer).expect("should exclusively own the signer"); + configured.large_duration = budget; + *configured + .namespace_gate + .lock() + .expect("should lock namespace gate") = Some(namespace_gate); + let cache = Arc::new(PendingLargeCache::default()); + let services = + build_services_with_http_client(Arc::clone(&origin) as Arc) + .with_media_signer(Some(Arc::clone(&signer) as Arc)) + .with_loading_cache(Arc::clone(&cache) as Arc); + let services = if has_timer { + services.with_timer(Arc::new(ElapsedTimer::default())) + } else { + services + }; + let claim = claim_for(&services, &Method::GET, &[]); + let response = not_modified(ETAG_V1); + let headers = response.headers().clone(); + let facts = OriginFacts::of(response.headers()); + + let delivery = claim + .deliver(&services, &identity(), Answered::Origin(facts), response) + .now_or_never() + .expect("should refuse before polling namespace without a timer and positive budget"); + + assert_unchanged_not_modified(delivery, &headers); + assert!( + signer + .namespace_gate + .lock() + .expect("should lock namespace gate") + .is_some(), + "should never call namespace resolution" + ); + assert_eq!(*cache.calls.lock().expect("should lock cache calls"), 0); + assert!(origin.requests().is_empty()); + assert_eq!(signer.attempts(), 0); + drop(namespace_ready); + } +} + +#[test] +fn a_namespace_pending_past_the_large_budget_starts_no_hash_or_signing() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let mut signer = LargeSigner::new(LargeBehavior::Sign); + let (namespace_ready, namespace_gate) = oneshot::channel(); + let configured = Arc::get_mut(&mut signer).expect("should exclusively own the signer"); + configured.large_duration = Duration::from_secs(5); + *configured + .namespace_gate + .lock() + .expect("should lock namespace gate") = Some(namespace_gate); + let timer = Arc::new(ElapsedTimer::default()); + let services = services_with_timer( + &signer, + &origin, + Arc::clone(&timer) as Arc, + ); + let claim = claim_for(&services, &Method::GET, &[]); + let response = whole(); + let facts = OriginFacts::of(response.headers()); + let Delivery::Large(answer) = + block_on(claim.deliver(&services, &identity(), Answered::Origin(facts), response)) + else { + panic!("should admit the source to the large lane"); + }; + let url = url::Url::parse(FETCHED_URL).expect("should parse the fetched address"); + let headers = HeaderMap::new(); + let pinned = PinnedOrigin { + url: &url, + headers: &headers, + backend_name: "origin", + auth: None, + unsigned_headers: &[], + response_header_rewrite: None, + }; + + // Keep the policy future unresolved while the deterministic large timer + // expires. A missing namespace race must fail here, never hang the test. + let response = claim + .deliver_large(&services, &identity(), pinned, answer) + .now_or_never() + .expect("should return unsigned when the large budget expires during namespace resolution"); + let (response, body) = collect(response); + + assert_eq!(body, Some(source()), "should preserve the original body"); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + assert_eq!(header_of(&response, "link"), None); + assert_eq!(header_of(&response, "repr-digest"), None); + assert!(origin.requests().is_empty(), "should never start hashing"); + assert_eq!(signer.attempts(), 0, "should never start signing"); + assert!(signer.jobs().is_empty()); + let requested = timer.requested.lock().expect("should lock timer requests"); + assert_eq!(requested.len(), 1, "should race the namespace once"); + assert!( + requested[0] > Duration::ZERO && requested[0] <= Duration::from_secs(5), + "should use the large budget rather than the policy's own hold limit" + ); + drop(namespace_ready); +} + +#[test] +fn an_exhausted_large_budget_starts_no_hash_or_signing() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_with_timer(&signer, &origin, Arc::new(ElapsedTimer::default())); + let load = LargeLoad { + signer: Arc::clone(&signer) as Arc, + services, + url: url::Url::parse(FETCHED_URL).expect("should parse the fetched address"), + headers: HeaderMap::new(), + backend_name: "origin".to_owned(), + auth: None, + unsigned_headers: Vec::new(), + version: SourceVersion::new(ETAG_V1, to_u64(source().len())) + .expect("should pin the test version"), + reader_url: READER_URL.to_owned(), + content_type: "image/png".to_owned(), + references: LargeReferences::of(&identity(), false), + namespace: "policy-1".to_owned(), + budget: LargeBudget { + started: Instant::now(), + total: Duration::ZERO, + }, + keep_for: Duration::from_secs(600), + }; + + let loaded = block_on(load.run()); + + assert!( + matches!( + decode_kept_outcome(&loaded.bytes), + Some(KeptOutcome::Declined) + ), + "should keep an unsigned outcome" + ); + assert!(origin.requests().is_empty(), "should never start hashing"); + assert_eq!(signer.attempts(), 0, "should never start signing"); +} + +#[test] +fn a_timed_out_source_is_hashed_once_and_then_sent_as_it_came() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Hang); + let services = services_with_timer(&signer, &origin, Arc::new(ElapsedTimer::default())); + let claim = claim_for(&services, &Method::GET, &[]); + + let (large, response) = run(&claim, &services, whole()); + let (response, body) = collect(response); + assert!(large); + assert!( + signer.jobs().is_empty(), + "a signer out of time signs nothing" + ); + assert_eq!(body, Some(source())); + assert_eq!(header_of(&response, "link"), None); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + let reads = origin.requests().len(); + + let (_, response) = run(&claim, &services, whole()); + let (_, body) = collect(response); + assert_eq!(body, Some(source())); + assert_eq!( + signer.attempts(), + 1, + "a kept timeout should never call the signer again" + ); + assert_eq!( + origin.requests().len(), + reads, + "a kept timeout should never hash again" + ); +} + +#[test] +fn a_version_change_before_publication_signs_nothing_and_sends_the_current_version() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + // The hash reads the first version; the signer's confirmation and the + // fresh answer meet the second. + origin.flip_after(1, &other_source(), ETAG_V2); + let signer = LargeSigner::new(LargeBehavior::ConfirmThenSign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + + let (large, response) = run(&claim, &services, whole()); + let (response, body) = collect(response); + + assert!(large); + assert!(signer.jobs().is_empty(), "should publish nothing"); + assert_eq!(signer.failures(), vec!["source_version_changed"]); + assert_eq!( + response.status(), + StatusCode::OK, + "an unsigned answer is the origin's, not a refusal" + ); + assert_eq!( + body, + Some(other_source()), + "should send the version the origin now serves" + ); + assert_eq!( + header_of(&response, "etag"), + Some(ETAG_V2), + "should describe the bytes sent by their own validator" + ); + assert_eq!(header_of(&response, "link"), None); + assert_eq!(header_of(&response, "repr-digest"), None); +} + +#[test] +fn a_fresh_fallback_is_never_cached_and_keeps_both_security_policies() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let caller_policy = "img-src 'self'"; + let fresh_policy = "default-src 'none'"; + // The same version answers afresh with a shorter lifetime and a + // security policy of its own. + origin.flip_after_with( + 1, + &source(), + ETAG_V1, + vec![ + ( + header::CACHE_CONTROL, + HeaderValue::from_static("public, max-age=5"), + ), + ( + header::CONTENT_SECURITY_POLICY, + HeaderValue::from_static(fresh_policy), + ), + ], + ); + let signer = LargeSigner::new(LargeBehavior::Refuse); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let mut answer = whole(); + answer.headers_mut().insert( + header::CONTENT_SECURITY_POLICY, + HeaderValue::from_static(caller_policy), + ); + + let (_, response) = run(&claim, &services, answer); + let (response, body) = collect(response); + + assert_eq!(body, Some(source())); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + assert_eq!( + header_of(&response, "cache-control"), + Some(crate::cache_policy::NO_STORE_PRIVATE_CACHE_CONTROL), + "should keep neither the first answer's lifetime nor the fresh one's" + ); + let policies: Vec<_> = response + .headers() + .get_all(header::CONTENT_SECURITY_POLICY) + .iter() + .collect(); + assert_eq!( + policies, + vec![caller_policy, fresh_policy], + "should add the fresh policy to the caller's, never replace it" + ); +} + +#[test] +fn a_refused_source_that_changed_keeps_the_new_versions_provenance_and_privacy() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let origin_link = "; rel=\"c2pa-manifest\""; + // The hash reads the first version; the fresh answer meets a second + // that names a manifest of its own and may be held by no shared cache. + origin.flip_after_with( + 1, + &other_source(), + ETAG_V2, + vec![ + ( + header::CACHE_CONTROL, + HeaderValue::from_static("private, no-store"), + ), + (header::LINK, HeaderValue::from_static(origin_link)), + ( + header::SET_COOKIE, + HeaderValue::from_static("session=reader-1"), + ), + ], + ); + let signer = LargeSigner::new(LargeBehavior::Refuse); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let mut answer = whole(); + answer.headers_mut().insert( + header::EXPIRES, + HeaderValue::from_static("Wed, 21 Oct 2026 07:28:00 GMT"), + ); + answer + .headers_mut() + .insert(header::AGE, HeaderValue::from_static("30")); + + let (_, response) = run(&claim, &services, answer); + let (response, body) = collect(response); + + assert_eq!(signer.attempts(), 1); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + body, + Some(other_source()), + "should send the version the origin now serves" + ); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V2)); + let links: Vec<_> = response.headers().get_all(header::LINK).iter().collect(); + assert_eq!( + links, + vec![origin_link], + "should keep the new version's manifest link" + ); + assert_eq!( + header_of(&response, "cache-control"), + Some(crate::cache_policy::NO_STORE_PRIVATE_CACHE_CONTROL), + "should never send the first version's shared cache policy with other bytes" + ); + assert_eq!(header_of(&response, "expires"), None); + assert_eq!(header_of(&response, "age"), None); + assert_eq!( + header_of(&response, "set-cookie"), + None, + "should pass on no cookie the caller's handling never saw" + ); + assert_eq!(header_of(&response, "repr-digest"), None); +} + +#[test] +fn a_fresh_fallback_keeps_redirect_challenge_and_retry_response_semantics() { + for (status, name, value) in [ + ( + StatusCode::TEMPORARY_REDIRECT, + header::LOCATION, + "/media/moved", + ), + ( + StatusCode::UNAUTHORIZED, + header::WWW_AUTHENTICATE, + "Bearer realm=\"media\"", + ), + (StatusCode::SERVICE_UNAVAILABLE, header::RETRY_AFTER, "60"), + ] { + let origin = RangeOrigin::new(&source(), ETAG_V1); + origin.flip_after_with( + 1, + &source(), + ETAG_V1, + vec![(name.clone(), HeaderValue::from_static(value))], + ); + origin + .state + .lock() + .expect("should lock origin") + .status_after = Some((1, status)); + let signer = LargeSigner::new(LargeBehavior::Refuse); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let (_, response) = run(&claim, &services, whole()); + let (response, _) = collect(response); + + assert_eq!(response.status(), status); + assert_eq!( + response.headers().get(&name), + Some(&HeaderValue::from_static(value)) + ); + assert_eq!( + header_of(&response, "cache-control"), + Some(crate::cache_policy::NO_STORE_PRIVATE_CACHE_CONTROL), + ); + assert_eq!(signer.attempts(), 1); + assert_eq!( + origin.requests().len(), + 2, + "should not follow or retry the fresh response" + ); + } +} + +#[test] +fn a_fresh_publisher_redirect_uses_the_existing_origin_url_mapping_without_discarded_cookies() { + for (location, expected) in [ + ( + "https://origin.example.com/media/moved", + "https://www.example.com/media/moved", + ), + (FETCHED_URL, FETCHED_URL), + ] { + let origin = RangeOrigin::new(&source(), ETAG_V1); + origin.flip_after_with( + 1, + &source(), + ETAG_V1, + vec![ + (header::LOCATION, HeaderValue::from_static(location)), + ( + header::SET_COOKIE, + HeaderValue::from_static("session=new; Path=/"), + ), + ( + header::CONTENT_LOCATION, + HeaderValue::from_static("https://origin.example.com/media/fresh"), + ), + ], + ); + origin + .state + .lock() + .expect("should lock origin") + .status_after = Some((1, StatusCode::TEMPORARY_REDIRECT)); + let signer = LargeSigner::new(LargeBehavior::Refuse); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let publisher = TestOrigin { + response_header_rewrite: Some(crate::response_header_rewrite::OriginHeaderRewrite { + origin_host: "origin.example.com", + origin_host_header: "origin.example.com", + origin_path: "", + request_host: "www.example.com", + request_scheme: "https", + origin_scheme: "https", + request_path_and_query: PATH, + request_is_get_or_head: true, + }), + ..TestOrigin::plain() + }; + let mut initial = whole(); + initial + .headers_mut() + .insert(header::CONTENT_LOCATION, HeaderValue::from_static("/stale")); + let (_, response) = run_through(&claim, &services, &publisher, initial); + assert_eq!(response.status(), StatusCode::TEMPORARY_REDIRECT); + assert_eq!( + response.headers().get(header::LOCATION), + Some(&HeaderValue::from_static(expected)), + "a discarded cookie must not justify redirecting to the same serving URL", + ); + assert!(!response.headers().contains_key(header::SET_COOKIE)); + assert_eq!( + response.headers().get(header::CONTENT_LOCATION), + Some(&HeaderValue::from_static( + "https://www.example.com/media/fresh" + )), + ); + } +} + +#[test] +fn a_version_change_after_signing_is_refused_rather_than_mixed() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + // The hash reads the first version; the signed delivery meets the + // second. + origin.flip_after(1, &other_source(), ETAG_V2); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + + let (_, response) = run(&claim, &services, whole()); + let (response, body) = collect(response); + + assert_eq!(signer.jobs().len(), 1); + assert_eq!( + response.status(), + StatusCode::SERVICE_UNAVAILABLE, + "should never send other bytes under the signed version's headers" + ); + assert_eq!(header_of(&response, "link"), None); + assert_eq!(body, Some(Vec::new())); +} + +#[test] +fn a_cut_hash_read_signs_nothing_and_is_never_resumed() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + origin.cut_next_at(23); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + + let (large, response) = run(&claim, &services, whole()); + let (response, body) = collect(response); + + assert!(large); + assert_eq!(signer.attempts(), 0, "should sign nothing from a cut read"); + assert_eq!( + origin.ranges(), + vec![Some((0, 63)), None], + "should neither resume nor restart the hash, only ask the origin afresh once as the reader asked" + ); + assert_eq!( + body, + Some(source()), + "should deliver the origin's fresh answer" + ); + assert_eq!( + header_of(&response, "link"), + None, + "should carry no manifest" + ); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + + let (_, response) = run(&claim, &services, whole()); + let (_, body) = collect(response); + assert_eq!(body, Some(source())); + assert_eq!( + origin.requests().len(), + 2, + "a kept failure should never hash again" + ); + + let origin = RangeOrigin::new(&source(), ETAG_V1); + origin.cut_next_at(23); + origin.flip_after(1, &other_source(), ETAG_V2); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let (_, response) = run(&claim, &services, whole()); + let (response, body) = collect(response); + assert_eq!(signer.attempts(), 0); + assert_eq!( + response.status(), + StatusCode::OK, + "an unsigned answer is the origin's, not a refusal" + ); + assert_eq!(body, Some(other_source())); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V2)); + assert_eq!(header_of(&response, "link"), None); +} + +#[test] +fn a_source_no_public_address_names_is_never_hashed() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let queried = TestOrigin { + identity: VariantIdentity { + fetched_url: QUERY_FETCHED_URL, + reader_query: "v=2", + ..identity() + }, + ..TestOrigin::plain() + }; + + for _ in 0..2 { + let (_, response) = run_through(&claim, &services, &queried, whole()); + let (response, body) = collect(response); + assert_eq!( + body, + Some(source()), + "should send the origin's answer as it came" + ); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + assert_eq!(header_of(&response, "link"), None); + } + assert!( + origin.requests().is_empty(), + "should never read a source no public address names" + ); + assert_eq!(signer.attempts(), 0); + + let reader_named = TestOrigin { + identity: VariantIdentity { + fetched_url: QUERY_FETCHED_URL, + ..identity() + }, + ..TestOrigin::plain() + }; + let _ = collect(run_through(&claim, &services, &reader_named, whole()).1); + let jobs = signer.jobs(); + assert_eq!( + jobs[0].source_reference, None, + "should never name a fetched address with a query" + ); + assert_eq!( + jobs[0].reader_reference.as_deref(), + Some(READER_URL), + "should name the reader's query-free address" + ); +} + +#[test] +fn a_signed_origin_address_is_never_named_and_its_reads_carry_no_reader_header() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::ConfirmThenSign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let mut headers = HeaderMap::new(); + headers.insert(header::HOST, HeaderValue::from_static("origin.example.com")); + headers.insert( + header::REFERER, + HeaderValue::from_static("https://www.example.com/page"), + ); + headers.insert( + header::IF_NONE_MATCH, + HeaderValue::from_static("\"reader\""), + ); + headers.insert("x-test-origin-key", HeaderValue::from_static("from-reader")); + let signed = TestOrigin { + headers, + auth: Some(OriginSigning::new(Arc::new(TestOriginSigner), None)), + unsigned_headers: vec![header::REFERER], + ..TestOrigin::plain() + }; + + let (_, response) = run_through(&claim, &services, &signed, whole()); + let (response, body) = collect(response); + + let jobs = signer.jobs(); + assert_eq!( + jobs[0].source_reference, None, + "should never name an address core signs" + ); + assert_eq!(jobs[0].reader_reference.as_deref(), Some(READER_URL)); + assert_eq!(body, Some(source())); + assert_eq!(header_of(&response, "link"), Some(MANIFEST_LINK)); + let requests = origin.requests(); + assert_eq!(requests.len(), 3, "should hash, confirm and deliver"); + for request in &requests { + assert_eq!( + request + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()), + Some(ORIGIN_SIGNATURE), + "should sign every pinned read afresh" + ); + assert!( + request.get(header::REFERER).is_none(), + "should send no reader header a signed read must not carry" + ); + assert!( + request.get(header::IF_NONE_MATCH).is_none(), + "should send none of the reader's conditions" + ); + assert!( + request.get("x-test-origin-key").is_none(), + "should send no header in the signer's namespace" + ); + assert_eq!( + request + .get(header::IF_MATCH) + .and_then(|value| value.to_str().ok()), + Some(ETAG_V1) + ); + } + + let queried_reader = TestOrigin { + identity: VariantIdentity { + reader_query: "v=2", + ..identity() + }, + auth: Some(OriginSigning::new(Arc::new(TestOriginSigner), None)), + ..TestOrigin::plain() + }; + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let _ = collect(run_through(&claim, &services, &queried_reader, whole()).1); + assert!( + origin.requests().is_empty(), + "should decline before hashing when only the signed address could name the source" + ); + assert_eq!(signer.attempts(), 0); +} + +#[test] +fn an_answer_whose_outcome_cannot_be_kept_is_never_hashed() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + for cache_control in ["no-cache", "public"] { + let mut answer = whole(); + answer.headers_mut().insert( + header::CACHE_CONTROL, + HeaderValue::from_static(cache_control), + ); + let (large, response) = run(&claim, &services, answer); + let (_, body) = collect(response); + assert!(!large, "{cache_control}: should not enter the large lane"); + assert_eq!(body, Some(source())); + } + + let keeps_nothing = + build_services_with_http_client(Arc::clone(&origin) as Arc) + .with_media_signer(Some(Arc::clone(&signer) as Arc)) + .with_timer(Arc::new(StoppedTimer)); + let claim = claim_for(&keeps_nothing, &Method::GET, &[]); + let (large, response) = run(&claim, &keeps_nothing, whole()); + let (_, body) = collect(response); + assert!( + !large, + "should not enter the large lane where nothing is kept" + ); + assert_eq!(body, Some(source())); + + assert!( + origin.requests().is_empty(), + "should never read the origin again" + ); + assert_eq!(signer.attempts(), 0); +} + +#[test] +fn an_origin_manifest_link_is_kept_rather_than_competed_with() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let origin_link = "; rel=\"c2pa-manifest\""; + let mut answer = whole(); + answer + .headers_mut() + .insert(header::LINK, HeaderValue::from_static(origin_link)); + + let (large, response) = run(&claim, &services, answer); + let (response, body) = collect(response); + + assert!( + !large, + "should not sign a source that names its own manifest" + ); + assert_eq!(body, Some(source())); + let links: Vec<_> = response.headers().get_all(header::LINK).iter().collect(); + assert_eq!( + links, + vec![origin_link], + "should keep only the origin's manifest link" + ); + assert!(origin.requests().is_empty()); + assert_eq!(signer.attempts(), 0); +} + +#[test] +fn a_warm_not_modified_naming_an_origin_manifest_gets_no_kept_outcome() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let _ = collect(run(&claim, &services, whole()).1); + assert_eq!( + signer.jobs().len(), + 1, + "should keep a signed outcome for the version" + ); + + let (_, plain) = run(&claim, &services, not_modified(ETAG_V1)); + assert_eq!( + header_of(&plain, "link"), + Some(MANIFEST_LINK), + "the kept outcome decorates a 304 without a link of the origin's" + ); + + let origin_link = "; rel=\"c2pa-manifest\""; + let mut linked = not_modified(ETAG_V1); + linked + .headers_mut() + .insert(header::LINK, HeaderValue::from_static(origin_link)); + let (_, response) = run(&claim, &services, linked); + + assert_eq!(response.status(), StatusCode::NOT_MODIFIED); + let links: Vec<_> = response.headers().get_all(header::LINK).iter().collect(); + assert_eq!( + links, + vec![origin_link], + "should keep the origin's manifest link alone" + ); + assert_eq!(header_of(&response, "repr-digest"), None); + assert_eq!(header_of(&response, "etag"), Some(ETAG_V1)); + assert_eq!(signer.attempts(), 1, "should never sign again"); +} + +#[test] +fn a_source_without_a_strong_tag_is_never_given_a_version() { + for etag in ["W/\"v1\"", ""] { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let claim = claim_for(&services, &Method::GET, &[]); + let mut answer = whole(); + if etag.is_empty() { + answer.headers_mut().remove(header::ETAG); + } else { + answer + .headers_mut() + .insert(header::ETAG, HeaderValue::from_static("W/\"v1\"")); + } + + let (large, response) = run(&claim, &services, answer); + let (response, body) = collect(response); + + assert!(!large, "{etag:?}: should not enter the large lane"); + assert_eq!(signer.attempts(), 0); + assert!( + origin.requests().is_empty(), + "{etag:?}: should not ask the origin again" + ); + assert_eq!( + body, + Some(source()), + "{etag:?}: should send the origin's answer as it came" + ); + assert_eq!(header_of(&response, "link"), None); + } +} + +#[test] +fn a_pinned_read_refuses_another_version_or_range() { + let origin = RangeOrigin::new(&source(), ETAG_V1); + let signer = LargeSigner::new(LargeBehavior::Sign); + let services = services_for(&signer, &origin); + let url = url::Url::parse(FETCHED_URL).expect("should parse the fetched address"); + let headers = HeaderMap::new(); + let version = SourceVersion::new(ETAG_V1, 64).expect("should take a strong tag"); + let reader = VersionedMediaSource::new(&services, &url, &headers, "origin", version); + + let mut part = [0_u8; 8]; + block_on(reader.read_exact_at(8, &mut part)).expect("should read the pinned range"); + assert_eq!(part.to_vec(), source()[8..16].to_vec()); + assert_eq!( + block_on(reader.read_exact_at(60, &mut part)), + Err(MediaSourceError::OutOfBounds), + "should refuse a read past the version before asking" + ); + + origin.answer_wrong_content_range(); + assert_eq!( + block_on(reader.read_exact_at(8, &mut part)), + Err(MediaSourceError::InvalidAnswer { + reason: "pinned_read_content_range" + }) + ); + + origin.flip_after(0, &other_source(), ETAG_V2); + assert_eq!( + block_on(reader.confirm_current()), + Err(MediaSourceError::VersionChanged) + ); + assert!( + SourceVersion::new("W/\"v1\"", 64).is_none(), + "a weak tag names no version" + ); +} diff --git a/crates/trusted-server-core/src/media_signing/lookup_tests.rs b/crates/trusted-server-core/src/media_signing/lookup_tests.rs new file mode 100644 index 0000000000..034075ad36 --- /dev/null +++ b/crates/trusted-server-core/src/media_signing/lookup_tests.rs @@ -0,0 +1,454 @@ +//! Core with a lookup-only signer, as the asynchronous prepared mode runs +//! it, and with a request its module hooks marked as a preparation +//! snapshot. +//! +//! A lookup-only signer is never given work core keeps or shares: core +//! reads and writes no kept signed representation, so no reader starts or +//! joins a load. A snapshot read gets the origin's answer exactly as it +//! came, with only the signer's facts and a private transport header added. + +use std::sync::Arc; + +use base64::engine::general_purpose::STANDARD; +use bytes::Bytes; +use edgezero_core::body::Body as EdgeBody; +use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, StatusCode, header}; +use sha2::Sha256; + +use super::test_support::{Behavior, StubSigner}; +use super::*; +use crate::integrations::IntegrationRequestState; +use crate::platform::test_support::{MemoryLoadingCache, noop_services}; +use crate::test_support::tests::create_test_settings; + +const SOURCE: &[u8] = b"origin-image-bytes"; +const PATH: &str = "/media/42"; +const FACTS: &str = "x-test-snapshot-facts"; +const VARIANT_SOURCE: &[u8] = b"variant-image-data"; +const BASE_LINK: &str = "; rel=\"c2pa-manifest\""; +const VARIANT_LINK: &str = "; rel=\"c2pa-manifest\""; + +/// What a module's request preparer leaves on a snapshot read. +struct SnapshotMark; + +/// [`StubSigner`] as a lookup-only signer that observes marked snapshots. +struct LookupOnly(Arc); + +impl MediaSigner for LookupOnly { + fn signer_id(&self) -> &'static str { + self.0.signer_id() + } + + fn handles_path(&self, path: &str) -> bool { + self.0.handles_path(path) + } + + fn handles_media_type(&self, media_type: &str) -> bool { + self.0.handles_media_type(media_type) + } + + fn max_source_bytes(&self) -> usize { + self.0.max_source_bytes() + } + + fn max_large_source_bytes(&self) -> u64 { + 1_000_000 + } + + fn namespace<'a>(&'a self, services: &'a RuntimeServices) -> MediaNamespacing<'a> { + self.0.namespace(services) + } + + fn sign<'a>( + &'a self, + services: &'a RuntimeServices, + job: &'a MediaJob<'a>, + ) -> MediaSigning<'a> { + self.0.sign(services, job) + } + + fn lookup_only(&self) -> bool { + true + } + + fn lookup_large<'a>( + &'a self, + _services: &'a RuntimeServices, + job: &'a LargeLookupJob<'a>, + ) -> LargeLookup<'a> { + Box::pin(async move { + let (source, link) = match job.reader_query { + "" => (SOURCE, BASE_LINK), + "asset=b" => (VARIANT_SOURCE, VARIANT_LINK), + _ => { + return Err(MediaSigningFailure { + reason: "not_prepared", + }); + } + }; + Ok(PublishedLarge { + headers: vec![(header::LINK, HeaderValue::from_static(link))], + sha256: Sha256::digest(source).into(), + length: u64::try_from(source.len()).expect("should fit source length"), + }) + }) + } + + fn observes_snapshot(&self, state: &IntegrationRequestState) -> bool { + state.get::("testing").is_some() + } + + fn snapshot_headers( + &self, + _state: &IntegrationRequestState, + facts: &SnapshotFacts, + ) -> Vec { + let value = format!( + "private={};encoding={};host={};link={}", + facts.private, facts.content_encoding, facts.origin_host, facts.manifest_link + ); + vec![ + ( + HeaderName::from_static(FACTS), + HeaderValue::from_str(&value).expect("should be a header value"), + ), + ( + HeaderName::from_static("x-test-snapshot-origin-identity"), + HeaderValue::from_str(&facts.origin_identity_sha256) + .expect("should encode the configured-source digest"), + ), + ] + } +} + +fn identity() -> VariantIdentity<'static> { + VariantIdentity { + reader_url: "https://www.example.com/media/42", + origin_identity: "https://origin.example.com\0origin.example.com", + fetched_url: "https://origin.example.com/media/42", + reader_query: "", + } +} + +fn image_request() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("sec-fetch-dest", HeaderValue::from_static("image")); + headers +} + +fn origin(extra: &[(HeaderName, &str)], body: Vec) -> Response { + let mut builder = Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "image/png") + .header(header::ETAG, "\"origin-etag\"") + .header(header::CACHE_CONTROL, "public, max-age=600"); + for (name, value) in extra { + builder = builder.header(name.clone(), *value); + } + builder + .body(EdgeBody::from(body)) + .expect("should build an origin answer") +} + +fn services_with(signer: Arc, cache: &Arc) -> RuntimeServices { + noop_services() + .with_media_signer(Some(signer)) + .with_loading_cache(cache.clone()) +} + +fn claim(services: &RuntimeServices) -> MediaClaim { + MediaClaim::of_request( + &create_test_settings(), + services, + &Method::GET, + PATH, + &image_request(), + true, + ) + .expect("should claim a media path") +} + +fn deliver( + claim: &MediaClaim, + services: &RuntimeServices, + response: Response, +) -> Delivery { + let facts = OriginFacts::of(response.headers()); + futures::executor::block_on(claim.deliver( + services, + &identity(), + Answered::Origin(facts), + response, + )) +} + +fn sent(delivery: Delivery) -> (Response, Bytes) { + let Delivery::Send(response) = delivery else { + panic!("should send a response"); + }; + let (parts, body) = response.into_parts(); + let bytes = futures::executor::block_on(body.into_bytes_bounded(1024 * 1024)) + .expect("should read the body"); + (Response::from_parts(parts, EdgeBody::empty()), bytes) +} + +#[test] +fn a_lookup_only_signer_never_reads_or_keeps_a_shared_signed_copy() { + // The same deployment with an ordinary signer keeps what it signs. + let ordinary = StubSigner::new(Behavior::Sign, 1024); + let ordinary_cache = Arc::new(MemoryLoadingCache::default()); + let ordinary_services = services_with(ordinary, &ordinary_cache); + let _ = sent(deliver( + &claim(&ordinary_services), + &ordinary_services, + origin(&[], SOURCE.to_vec()), + )); + assert!( + !ordinary_cache.calls().is_empty(), + "an ordinary signer's result is kept for other readers" + ); + + let stub = StubSigner::new(Behavior::Sign, 1024); + let cache = Arc::new(MemoryLoadingCache::default()); + let services = services_with(Arc::new(LookupOnly(Arc::clone(&stub))), &cache); + let claim = claim(&services); + assert!( + futures::executor::block_on(claim.kept(&services, &identity())).is_none(), + "nothing kept is looked up" + ); + let (response, body) = sent(deliver(&claim, &services, origin(&[], SOURCE.to_vec()))); + assert_eq!(response.status(), StatusCode::OK); + assert!( + body.starts_with(SOURCE), + "should deliver what the signer looked up for these bytes" + ); + assert_eq!(stub.jobs().len(), 1, "the signer is asked once, to look up"); + assert!( + cache.calls().is_empty(), + "no load is started, joined or kept: {:?}", + cache.calls() + ); +} + +#[test] +fn a_snapshot_read_gets_the_origin_answer_as_it_came_with_the_signer_s_facts() { + let stub = StubSigner::new(Behavior::Sign, 1024); + let cache = Arc::new(MemoryLoadingCache::default()); + let services = services_with(Arc::new(LookupOnly(Arc::clone(&stub))), &cache); + let mut request = Request::builder() + .uri(PATH) + .body(EdgeBody::empty()) + .expect("should build"); + IntegrationRequestState::observe(&mut request, "testing", SnapshotMark); + let snapshot = claim(&services).observing(&IntegrationRequestState::of(&request)); + + let (response, body) = sent(deliver( + &snapshot, + &services, + origin(&[(header::SET_COOKIE, "session=reader")], SOURCE.to_vec()), + )); + + assert_eq!(body.as_ref(), SOURCE, "the origin's exact bytes"); + assert!(stub.jobs().is_empty(), "nothing is signed or looked up"); + assert!(cache.calls().is_empty(), "nothing is kept"); + assert_eq!( + response + .headers() + .get(FACTS) + .and_then(|value| value.to_str().ok()), + Some("private=true;encoding=;host=origin.example.com;link=false"), + "the origin's own privacy is reported, read before core changed anything" + ); + assert_eq!( + response + .headers() + .get(header::CACHE_CONTROL) + .and_then(|value| value.to_str().ok()), + Some("private, no-store"), + "the snapshot's own transport is private, apart from the origin's facts" + ); + assert_eq!( + response + .headers() + .get(header::ETAG) + .and_then(|value| value.to_str().ok()), + Some("\"origin-etag\""), + "the origin's validators pass unchanged" + ); + + // An ordinary reader's request on the same deployment is no snapshot. + let reader = Request::builder() + .uri(PATH) + .body(EdgeBody::empty()) + .expect("should build"); + let ordinary = claim(&services).observing(&IntegrationRequestState::of(&reader)); + let (response, _) = sent(deliver(&ordinary, &services, origin(&[], SOURCE.to_vec()))); + assert!(response.headers().get(FACTS).is_none()); + assert_eq!( + stub.jobs().len(), + 1, + "a reader's answer goes to the signer's lookup" + ); +} + +#[test] +fn snapshot_origin_digest_tracks_configured_source_not_transport_host_or_validators() { + let stub = StubSigner::new(Behavior::Sign, 1024); + let cache = Arc::new(MemoryLoadingCache::default()); + let services = services_with(Arc::new(LookupOnly(Arc::clone(&stub))), &cache); + let mut request = Request::builder() + .uri(PATH) + .body(EdgeBody::empty()) + .expect("should build a snapshot request"); + IntegrationRequestState::observe(&mut request, "testing", SnapshotMark); + let snapshot = claim(&services).observing(&IntegrationRequestState::of(&request)); + let mut digests = Vec::new(); + for configured in [ + "https://origin.example.com/old/\0origin.example.com", + "https://origin.example.com/new/\0origin.example.com", + "https://origin.example.com/new/\0virtual.example.com", + "https://origin.example.com/new/\0/assets/\0virtual.example.com", + ] { + let identity = VariantIdentity { + origin_identity: configured, + ..identity() + }; + let response = origin(&[(header::CONTENT_LENGTH, "18")], SOURCE.to_vec()); + let facts = OriginFacts::of(response.headers()); + let delivery = futures::executor::block_on(snapshot.deliver( + &services, + &identity, + Answered::Origin(facts), + response, + )); + let (response, body) = sent(delivery); + assert_eq!(body.as_ref(), SOURCE); + assert_eq!( + response + .headers() + .get(header::ETAG) + .expect("should retain the origin validator"), + "\"origin-etag\"" + ); + let digest = response + .headers() + .get("x-test-snapshot-origin-identity") + .expect("should expose configured-source digest to the signer") + .to_str() + .expect("should be lowercase hex"); + assert_eq!(digest, hex::encode(Sha256::digest(configured.as_bytes()))); + assert_eq!(digest.len(), 64); + assert!(!digests.iter().any(|previous| previous == digest)); + digests.push(digest.to_owned()); + } + assert!(stub.jobs().is_empty()); + assert!(cache.calls().is_empty()); +} + +#[test] +fn a_lookup_only_signer_needs_no_timer_or_streaming_to_look_up_a_large_version() { + let stub = StubSigner::new(Behavior::Sign, 4); + let cache = Arc::new(MemoryLoadingCache::default()); + let services = services_with(Arc::new(LookupOnly(Arc::clone(&stub))), &cache); + assert!(services.timer().is_none(), "the services carry no timer"); + + let delivery = deliver( + &claim(&services), + &services, + origin(&[(header::CONTENT_LENGTH, "18")], SOURCE.to_vec()), + ); + + assert!( + matches!(delivery, Delivery::Large(_)), + "a large version is admitted for a lookup by its strong validator" + ); + assert!( + stub.jobs().is_empty(), + "nothing is signed while it is admitted" + ); + assert!(cache.calls().is_empty(), "no load is started or joined"); +} + +#[test] +fn large_reader_delivery_preserves_the_query_selected_resource_and_its_own_provenance() { + assert_eq!(SOURCE.len(), VARIANT_SOURCE.len()); + assert_ne!(SOURCE, VARIANT_SOURCE); + let stub = StubSigner::new(Behavior::Sign, 4); + let cache = Arc::new(MemoryLoadingCache::default()); + let services = services_with(Arc::new(LookupOnly(Arc::clone(&stub))), &cache); + let claim = claim(&services); + for (query, source, expected_link) in [ + ("", SOURCE, Some(BASE_LINK)), + ("asset=b", VARIANT_SOURCE, Some(VARIANT_LINK)), + ("unknown=b", VARIANT_SOURCE, None), + ("asset=b&asset=b", VARIANT_SOURCE, None), + ] { + let fetched_url = format!("https://origin.example.com/media/42?{query}"); + let identity = VariantIdentity { + reader_query: query, + fetched_url: &fetched_url, + ..identity() + }; + let response = origin(&[(header::CONTENT_LENGTH, "18")], source.to_vec()); + let facts = OriginFacts::of(response.headers()); + let delivery = futures::executor::block_on(claim.deliver( + &services, + &identity, + Answered::Origin(facts), + response, + )); + let Delivery::Large(answer) = delivery else { + panic!("should admit this source for large lookup"); + }; + let url = url::Url::parse(&fetched_url).expect("should parse source URL"); + let headers = HeaderMap::new(); + let response = futures::executor::block_on(claim.deliver_large( + &services, + &identity, + PinnedOrigin { + url: &url, + headers: &headers, + backend_name: "origin", + auth: None, + unsigned_headers: &[], + response_header_rewrite: None, + }, + answer, + )); + let (response, body) = sent(Delivery::Send(response)); + assert_eq!( + body.as_ref(), + source, + "{query}: should preserve origin bytes" + ); + assert_eq!( + response + .headers() + .get(header::LINK) + .and_then(|value| value.to_str().ok()), + expected_link, + "{query}: should select only this query resource's manifest" + ); + if expected_link.is_some() { + let digest = format!("sha-256=:{}:", STANDARD.encode(Sha256::digest(source))); + assert_eq!( + response + .headers() + .get("repr-digest") + .and_then(|value| value.to_str().ok()), + Some(digest.as_str()), + "{query}: should carry this resource's own digest" + ); + } else { + assert!( + response.headers().get("repr-digest").is_none(), + "{query}: should not carry another resource's digest" + ); + } + } + assert!(stub.jobs().is_empty(), "no source is signed during lookup"); + assert!( + cache.calls().is_empty(), + "no signed load is started or joined" + ); +} diff --git a/crates/trusted-server-core/src/media_signing/test_support.rs b/crates/trusted-server-core/src/media_signing/test_support.rs new file mode 100644 index 0000000000..f6e5ddb075 --- /dev/null +++ b/crates/trusted-server-core/src/media_signing/test_support.rs @@ -0,0 +1,164 @@ +//! A [`MediaSigner`] for core's tests, which records each job and appends a +//! fixed suffix to the source in place of a manifest. + +use std::sync::{Arc, Mutex}; + +use bytes::Bytes; +use http::{HeaderName, HeaderValue, header}; + +use super::{ + MediaJob, MediaNamespacing, MediaSigner, MediaSigning, MediaSigningFailure, SignedMedia, +}; +use crate::platform::RuntimeServices; + +/// What the stub's signing appends to the source. +pub(crate) const SIGNED_SUFFIX: &[u8] = b"|signed-manifest"; + +/// The header the stub writes on a signed representation. +pub(crate) const MANIFEST_HEADER: &str = "x-test-manifest"; + +/// The `Link` the stub's [`Behavior::SignWithLink`] writes. +pub(crate) const MANIFEST_LINK: &str = "; rel=\"c2pa-manifest\""; + +/// How the stub answers a job. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Behavior { + /// Returns the source with [`SIGNED_SUFFIX`] appended. + Sign, + /// Returns a failure. + Fail, + /// Returns signed bytes with a header core owns. + ReservedHeader, + /// Signs as [`Behavior::Sign`] and adds a manifest [`MANIFEST_LINK`]. + SignWithLink, +} + +/// One job the stub was handed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RecordedJob { + pub(crate) reader_url: String, + pub(crate) content_type: String, + pub(crate) source: Vec, + pub(crate) source_sha256: String, + pub(crate) source_reference: Option, + pub(crate) source_origin_host: Option, + pub(crate) optimizer_output: bool, +} + +/// Leaves `/api/` paths alone and signs every `image/` type. +pub(crate) struct StubSigner { + behavior: Behavior, + limit: usize, + jobs: Mutex>, + namespace: Mutex>, +} + +impl StubSigner { + pub(crate) fn new(behavior: Behavior, limit: usize) -> Arc { + Arc::new(Self { + behavior, + limit, + jobs: Mutex::new(Vec::new()), + namespace: Mutex::new(Ok("policy-1".to_owned())), + }) + } + + /// Makes [`MediaSigner::namespace`] answer `namespace` from now on, as a + /// changed or revoked policy would. + pub(crate) fn set_namespace(&self, namespace: Result) { + *self.namespace.lock().expect("should lock namespace") = namespace; + } + + pub(crate) fn jobs(&self) -> Vec { + self.jobs.lock().expect("should lock jobs").clone() + } + + /// `services` carrying this signer, as an adapter hands it in. + pub(crate) fn installed_in(self: &Arc, services: RuntimeServices) -> RuntimeServices { + services.with_media_signer(Some(Arc::clone(self) as Arc)) + } +} + +impl MediaSigner for StubSigner { + fn signer_id(&self) -> &'static str { + "testing.media" + } + + fn handles_path(&self, path: &str) -> bool { + !path.starts_with("/api/") + } + + fn handles_media_type(&self, media_type: &str) -> bool { + media_type.starts_with("image/") + } + + fn max_source_bytes(&self) -> usize { + self.limit + } + + fn namespace<'a>(&'a self, _services: &'a RuntimeServices) -> MediaNamespacing<'a> { + let namespace = self + .namespace + .lock() + .expect("should lock namespace") + .clone(); + Box::pin(async move { namespace }) + } + + fn sign<'a>( + &'a self, + _services: &'a RuntimeServices, + job: &'a MediaJob<'a>, + ) -> MediaSigning<'a> { + Box::pin(async move { + self.jobs + .lock() + .expect("should lock jobs") + .push(RecordedJob { + reader_url: job.reader_url.to_owned(), + content_type: job.content_type.to_owned(), + source: job.source.to_vec(), + source_sha256: job.source_sha256.to_owned(), + source_reference: job.source_reference.map(str::to_owned), + source_origin_host: job.source_origin_host.map(str::to_owned), + optimizer_output: job.optimizer_output, + }); + // Signs under the namespace current when it signs, as a real + // signer reports the policy it used. + let namespace = self + .namespace + .lock() + .expect("should lock namespace") + .clone() + .unwrap_or_default(); + let mut bytes = job.source.to_vec(); + bytes.extend_from_slice(SIGNED_SUFFIX); + match self.behavior { + Behavior::Fail => Err(MediaSigningFailure { + reason: "stub_failure", + }), + Behavior::Sign => Ok(SignedMedia { + bytes: Bytes::from(bytes), + content_type: HeaderValue::from_static("image/png"), + headers: vec![( + HeaderName::from_static(MANIFEST_HEADER), + HeaderValue::from_static("instance-1"), + )], + namespace, + }), + Behavior::SignWithLink => Ok(SignedMedia { + bytes: Bytes::from(bytes), + content_type: HeaderValue::from_static("image/png"), + headers: vec![(header::LINK, HeaderValue::from_static(MANIFEST_LINK))], + namespace, + }), + Behavior::ReservedHeader => Ok(SignedMedia { + bytes: Bytes::from(bytes), + content_type: HeaderValue::from_static("image/png"), + headers: vec![(header::ETAG, HeaderValue::from_static("\"forged\""))], + namespace, + }), + } + }) + } +} diff --git a/crates/trusted-server-core/src/media_signing/tests.rs b/crates/trusted-server-core/src/media_signing/tests.rs new file mode 100644 index 0000000000..70750f915c --- /dev/null +++ b/crates/trusted-server-core/src/media_signing/tests.rs @@ -0,0 +1,1572 @@ +use std::sync::Arc; + +use bytes::Bytes; +use edgezero_core::body::Body as EdgeBody; +use http::{HeaderMap, HeaderName, HeaderValue, Method, Response, StatusCode, header}; +use sha2::Sha256; + +use super::test_support::{Behavior, MANIFEST_LINK, SIGNED_SUFFIX, StubSigner}; +use super::*; +use crate::platform::test_support::{MemoryLoadingCache, noop_services}; +use crate::test_support::tests::create_test_settings; + +const SOURCE: &[u8] = b"origin-image-bytes"; +const PATH: &str = "/media/42"; +const FETCHED_URL: &str = "https://origin.example.com/media/42"; +const READER_URL: &str = "https://www.example.com/media/42"; +const ORIGIN_IDENTITY: &str = "https://origin.example.com\0origin.example.com"; +const ORIGIN_ETAG: &str = "\"origin-etag\""; + +fn identity() -> VariantIdentity<'static> { + VariantIdentity { + reader_url: READER_URL, + origin_identity: ORIGIN_IDENTITY, + fetched_url: FETCHED_URL, + reader_query: "", + } +} + +fn signed_bytes() -> Vec { + let mut bytes = SOURCE.to_vec(); + bytes.extend_from_slice(SIGNED_SUFFIX); + bytes +} + +fn signed_etag() -> String { + format!("\"sig-{}\"", hex::encode(Sha256::digest(signed_bytes()))) +} + +fn request_headers(pairs: &[(HeaderName, &str)]) -> HeaderMap { + let mut headers = HeaderMap::new(); + for (name, value) in pairs { + headers.append( + name.clone(), + HeaderValue::from_str(value).expect("should build header"), + ); + } + headers +} + +/// The headers of a browser asking for an image, which may be answered +/// from a kept copy. +fn image_request(pairs: &[(HeaderName, &str)]) -> HeaderMap { + let mut headers = request_headers(pairs); + headers.insert("sec-fetch-dest", HeaderValue::from_static("image")); + headers +} + +fn origin_response( + status: StatusCode, + body: EdgeBody, + extra: &[(HeaderName, &str)], +) -> Response { + let mut builder = Response::builder() + .status(status) + .header(header::CONTENT_TYPE, "image/png") + .header(header::ETAG, ORIGIN_ETAG) + .header(header::CACHE_CONTROL, "public, max-age=600"); + for (name, value) in extra { + builder = builder.header(name.clone(), *value); + } + builder.body(body).expect("should build origin response") +} + +fn whole_origin() -> Response { + origin_response(StatusCode::OK, EdgeBody::from(SOURCE.to_vec()), &[]) +} + +fn part_origin() -> Response { + origin_response( + StatusCode::PARTIAL_CONTENT, + EdgeBody::from(SOURCE[0..=3].to_vec()), + &[(header::CONTENT_RANGE, "bytes 0-3/18")], + ) +} + +async fn body_bytes(body: EdgeBody) -> Vec { + match body { + EdgeBody::Once(bytes) => bytes.to_vec(), + EdgeBody::Stream(mut stream) => { + let mut collected = Vec::new(); + while let Some(chunk) = stream.next().await { + collected.extend_from_slice(&chunk.expect("should read stream chunk")); + } + collected + } + } +} + +fn header<'a>(response: &'a Response, name: &str) -> Option<&'a str> { + response + .headers() + .get(name) + .and_then(|value| value.to_str().ok()) +} + +fn claim_shared( + services: &RuntimeServices, + method: &Method, + headers: &HeaderMap, + shared: bool, +) -> MediaClaim { + MediaClaim::of_request( + &create_test_settings(), + services, + method, + PATH, + headers, + shared, + ) + .expect("should claim a media path") +} + +#[test] +fn retained_media_uses_its_declared_cache_lifetimes() { + let mut settings = create_test_settings(); + settings.cache.declared.insert( + ASSETS_CACHE.to_owned(), + crate::settings::LoadingCacheConfig { + memory_seconds: Some(2), + store_seconds: Some(120), + store_idle_seconds: Some(240), + ..Default::default() + }, + ); + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = signer.installed_in(noop_services()); + let claim = MediaClaim::of_request( + &settings, + &services, + &Method::GET, + PATH, + &image_request(&[]), + true, + ) + .expect("should claim a media request"); + + assert_eq!(claim.spec.name, ASSETS_CACHE); + assert_eq!(claim.spec.lifetimes.memory, Duration::from_secs(2)); + assert_eq!(claim.spec.lifetimes.store, Duration::from_secs(120)); + assert_eq!(claim.spec.lifetimes.store_idle, Duration::from_secs(240)); +} + +fn claim(services: &RuntimeServices, method: &Method, headers: &HeaderMap) -> MediaClaim { + claim_shared(services, method, headers, true) +} + +fn keeping(signer: &Arc) -> RuntimeServices { + signer + .installed_in(noop_services()) + .with_loading_cache(Arc::new(MemoryLoadingCache::default())) +} + +fn split(response: Response) -> (Response, Vec) { + let (parts, body) = response.into_parts(); + let bytes = futures::executor::block_on(body_bytes(body)); + (Response::from_parts(parts, EdgeBody::empty()), bytes) +} + +/// What `deliver` decides for the origin's `response`, with the facts its +/// own headers give. +fn delivery( + claim: &MediaClaim, + services: &RuntimeServices, + response: Response, +) -> Delivery { + let facts = OriginFacts::of(response.headers()); + futures::executor::block_on(claim.deliver( + services, + &identity(), + Answered::Origin(facts), + response, + )) +} + +/// Delivers the origin's `response` to a reader whose request is `method` +/// with `headers`, expecting core to send it without a second fetch. +fn deliver( + signer: &Arc, + method: &Method, + headers: &HeaderMap, + response: Response, +) -> (Response, Vec) { + let services = signer.installed_in(noop_services()); + let claim = claim(&services, method, headers); + let Delivery::Send(response) = delivery(&claim, &services, response) else { + panic!("should send without asking the origin again"); + }; + split(response) +} + +#[test] +fn signs_the_exact_captured_bytes_and_owns_the_validators() { + let signer = StubSigner::new(Behavior::Sign, 1024); + + let (response, body) = deliver(&signer, &Method::GET, &HeaderMap::new(), whole_origin()); + + assert_eq!(response.status(), StatusCode::OK, "should answer 200"); + assert_eq!( + body, + signed_bytes(), + "should send the signer's bytes unchanged" + ); + let jobs = signer.jobs(); + assert_eq!(jobs.len(), 1, "should ask the signer once"); + assert_eq!( + jobs[0].source, SOURCE, + "should hand the signer the exact origin bytes" + ); + assert_eq!(jobs[0].source_sha256, hex::encode(Sha256::digest(SOURCE))); + assert_eq!( + jobs[0].reader_url, READER_URL, + "should name the reader's address" + ); + assert_eq!( + jobs[0].content_type, "image/png", + "should pass the origin type" + ); + assert_eq!(jobs[0].source_reference.as_deref(), Some(FETCHED_URL)); + assert_eq!( + jobs[0].source_origin_host.as_deref(), + Some("origin.example.com") + ); + assert_eq!(header(&response, "etag"), Some(signed_etag().as_str())); + assert_eq!( + header(&response, "repr-digest"), + Some( + format!( + "sha-256=:{}:", + base64::engine::general_purpose::STANDARD.encode(Sha256::digest(signed_bytes())) + ) + .as_str() + ), + "should describe the signed representation's digest" + ); + assert_eq!( + header(&response, "content-length"), + Some(signed_bytes().len().to_string().as_str()), + "should frame the signed length" + ); + assert_eq!( + header(&response, "accept-ranges"), + Some("none"), + "a signed representation nothing keeps offers no ranges" + ); + assert_eq!(header(&response, "x-test-manifest"), Some("instance-1")); + assert_eq!( + header(&response, "cache-control"), + Some("public, max-age=600") + ); +} + +#[test] +fn merges_the_signers_link_with_the_origins_links() { + let signer = StubSigner::new(Behavior::SignWithLink, 1024); + let origin_link = "; rel=preload; as=style"; + + let (response, _) = deliver( + &signer, + &Method::GET, + &HeaderMap::new(), + origin_response( + StatusCode::OK, + EdgeBody::from(SOURCE.to_vec()), + &[(header::LINK, origin_link)], + ), + ); + + let links: Vec<&str> = response + .headers() + .get_all(header::LINK) + .iter() + .filter_map(|value| value.to_str().ok()) + .collect(); + assert_eq!( + links, + vec![origin_link, MANIFEST_LINK], + "should keep the origin's link and add the manifest's" + ); +} + +#[test] +fn names_the_origin_host_even_where_the_reference_is_withheld() { + for (fetched_url, host) in [ + ( + "https://origin.example.com/media/42?signature=secret", + "origin.example.com", + ), + ( + "https://user:pass@Origin.Example.com/media/42", + "origin.example.com", + ), + ( + "https://origin.example.com:8443/media/42#part", + "origin.example.com", + ), + ] { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = signer.installed_in(noop_services()); + let claim = claim(&services, &Method::GET, &HeaderMap::new()); + let response = whole_origin(); + let facts = OriginFacts::of(response.headers()); + futures::executor::block_on(claim.deliver( + &services, + &VariantIdentity { + fetched_url, + ..identity() + }, + Answered::Origin(facts), + response, + )); + let job = &signer.jobs()[0]; + assert_eq!( + job.source_reference, None, + "should send no reference for {fetched_url}" + ); + assert_eq!( + job.source_origin_host.as_deref(), + Some(host), + "should name the host of {fetched_url}" + ); + } +} + +#[test] +fn answers_a_single_range_from_the_signed_bytes() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let total = signed_bytes().len(); + + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::RANGE, "bytes=2-5")]), + whole_origin(), + ); + + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!( + body, + signed_bytes()[2..=5], + "should send the asked bytes of the signed body" + ); + assert_eq!( + header(&response, "content-range"), + Some(format!("bytes 2-5/{total}").as_str()) + ); + assert_eq!(header(&response, "content-length"), Some("4")); +} + +#[test] +fn answers_suffix_and_open_ranges_from_the_signed_bytes() { + let total = signed_bytes().len(); + for (range, first) in [("bytes=-4", total - 4), ("bytes=10-", 10)] { + let signer = StubSigner::new(Behavior::Sign, 1024); + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::RANGE, range)]), + whole_origin(), + ); + assert_eq!( + response.status(), + StatusCode::PARTIAL_CONTENT, + "{range} should be partial" + ); + assert_eq!( + body, + signed_bytes()[first..], + "{range} should send the tail" + ); + assert_eq!( + header(&response, "content-range"), + Some(format!("bytes {first}-{}/{total}", total - 1).as_str()) + ); + } +} + +#[test] +fn refuses_a_range_past_the_signed_length() { + let signer = StubSigner::new(Behavior::Sign, 1024); + + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::RANGE, "bytes=1000-")]), + whole_origin(), + ); + + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + assert!(body.is_empty(), "should send no body with a 416"); + assert_eq!( + header(&response, "content-range"), + Some(format!("bytes */{}", signed_bytes().len()).as_str()) + ); +} + +#[test] +fn answers_several_ranges_and_malformed_ranges_with_the_whole_signed_body() { + for range in ["bytes=0-1,4-5", "bytes=5-2", "items=0-1", "bytes=+1-2"] { + let signer = StubSigner::new(Behavior::Sign, 1024); + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::RANGE, range)]), + whole_origin(), + ); + assert_eq!( + response.status(), + StatusCode::OK, + "{range} should be answered whole" + ); + assert_eq!(body, signed_bytes()); + } +} + +#[test] +fn honors_if_range_only_for_the_signed_entity_tag() { + let signed = signed_etag(); + for (if_range, expected) in [ + (signed.as_str(), StatusCode::PARTIAL_CONTENT), + (ORIGIN_ETAG, StatusCode::OK), + ("Tue, 01 Sep 2026 00:00:00 GMT", StatusCode::OK), + ] { + let signer = StubSigner::new(Behavior::Sign, 1024); + let (response, _) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::RANGE, "bytes=0-3"), (header::IF_RANGE, if_range)]), + whole_origin(), + ); + assert_eq!(response.status(), expected, "If-Range {if_range}"); + } +} + +#[test] +fn tells_a_precondition_naming_signed_bytes_from_one_naming_the_origins() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = signer.installed_in(noop_services()); + let signed = signed_etag(); + for (headers, names_signed) in [ + ( + request_headers(&[ + (header::RANGE, "bytes=0-3"), + (header::IF_RANGE, signed.as_str()), + ]), + true, + ), + ( + request_headers(&[(header::IF_MATCH, format!("\"other\", {signed}").as_str())]), + true, + ), + ( + request_headers(&[ + (header::RANGE, "bytes=0-3"), + (header::IF_RANGE, ORIGIN_ETAG), + ]), + false, + ), + ( + request_headers(&[(header::IF_NONE_MATCH, signed.as_str())]), + false, + ), + ( + request_headers(&[(header::IF_MATCH, "W/\"sig-00\"")]), + false, + ), + ] { + assert_eq!( + claim(&services, &Method::GET, &headers).names_signed_bytes(), + names_signed, + "{headers:?}" + ); + } +} + +#[test] +fn answers_if_none_match_against_the_signed_tag_only() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[( + header::IF_NONE_MATCH, + format!("\"other\", W/{}", signed_etag()).as_str(), + )]), + whole_origin(), + ); + assert_eq!( + response.status(), + StatusCode::NOT_MODIFIED, + "a weak match of the signed tag is a 304" + ); + assert!(body.is_empty(), "a 304 carries no body"); + assert_eq!(header(&response, "content-length"), None); + assert_eq!(header(&response, "etag"), Some(signed_etag().as_str())); + assert_eq!( + header(&response, "x-test-manifest"), + Some("instance-1"), + "a 304 carries the signer's headers" + ); + + let signer = StubSigner::new(Behavior::Sign, 1024); + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::IF_NONE_MATCH, ORIGIN_ETAG)]), + whole_origin(), + ); + assert_eq!( + response.status(), + StatusCode::OK, + "the origin's tag never matches signed bytes" + ); + assert_eq!(body, signed_bytes()); +} + +#[test] +fn fails_if_match_that_names_another_representation() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::IF_MATCH, ORIGIN_ETAG)]), + whole_origin(), + ); + assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED); + assert!(body.is_empty()); + + let signer = StubSigner::new(Behavior::Sign, 1024); + let (response, _) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::IF_MATCH, signed_etag().as_str())]), + whole_origin(), + ); + assert_eq!( + response.status(), + StatusCode::OK, + "the signed tag satisfies If-Match" + ); +} + +#[test] +fn signs_the_whole_behind_a_part_that_can_be_kept_and_ranges_the_signed_bytes() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + let headers = image_request(&[(header::RANGE, "bytes=0-3")]); + let claim = claim(&services, &Method::GET, &headers); + + let Delivery::FetchWhole(fallback) = delivery(&claim, &services, part_origin()) else { + panic!("should ask for the whole representation of a part it can keep"); + }; + assert!(signer.jobs().is_empty(), "a part is never signed"); + + let delivered = futures::executor::block_on(claim.deliver_whole( + &services, + &identity(), + whole_origin(), + fallback, + )); + let (response, body) = split(delivered); + assert_eq!( + signer.jobs()[0].source, + SOURCE, + "should sign the whole answer's exact bytes" + ); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!( + body, + signed_bytes()[0..=3], + "should range the signed bytes, not the origin's" + ); + assert_eq!(header(&response, "etag"), Some(signed_etag().as_str())); + assert_eq!( + header(&response, "accept-ranges"), + Some("bytes"), + "a kept representation offers ranges" + ); +} + +#[test] +fn passes_a_part_through_where_nothing_would_keep_the_signed_whole() { + let cases: [(RuntimeServices, Response, &str); 3] = [ + ( + StubSigner::new(Behavior::Sign, 1024).installed_in(noop_services()), + part_origin(), + "no cache keeps values", + ), + ( + keeping(&StubSigner::new(Behavior::Sign, 1024)), + origin_response( + StatusCode::PARTIAL_CONTENT, + EdgeBody::from(SOURCE[0..=3].to_vec()), + &[ + (header::CONTENT_RANGE, "bytes 0-3/18"), + (header::CACHE_CONTROL, "no-cache"), + ], + ), + "the origin gave no shared freshness", + ), + ( + keeping(&StubSigner::new(Behavior::Sign, 1024)), + origin_response( + StatusCode::PARTIAL_CONTENT, + EdgeBody::from(b"part".to_vec()), + &[(header::CONTENT_RANGE, "bytes 0-3/50000000")], + ), + "the whole is over the limit", + ), + ]; + for (services, part, case) in cases { + let claim = claim( + &services, + &Method::GET, + &image_request(&[(header::RANGE, "bytes=0-3")]), + ); + let Delivery::Send(response) = delivery(&claim, &services, part) else { + panic!("{case}: should not read the whole origin for a part"); + }; + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT, "{case}"); + assert_eq!( + header(&response, "etag"), + Some(ORIGIN_ETAG), + "{case}: keeps the origin's part" + ); + } +} + +#[test] +fn asks_for_the_signed_tail_the_origin_refused_as_past_its_own_length() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = signer.installed_in(noop_services()); + let tail = signed_bytes().len() - 2; + let range = format!("bytes={tail}-"); + let headers = request_headers(&[(header::RANGE, range.as_str())]); + let claim = claim(&services, &Method::GET, &headers); + let refusal = origin_response( + StatusCode::RANGE_NOT_SATISFIABLE, + EdgeBody::empty(), + &[(header::CONTENT_RANGE, "bytes */18")], + ); + + let Delivery::FetchWhole(fallback) = delivery(&claim, &services, refusal) else { + panic!("a range past the origin's length may name the signed tail"); + }; + let delivered = futures::executor::block_on(claim.deliver_whole( + &services, + &identity(), + whole_origin(), + fallback, + )); + let (response, body) = split(delivered); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!(body, signed_bytes()[tail..], "should send the signed tail"); +} + +#[test] +fn sends_the_origins_part_without_validators_when_the_whole_fetch_cannot_be_signed() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + let claim = claim( + &services, + &Method::GET, + &image_request(&[(header::RANGE, "bytes=0-3")]), + ); + + let mut missing = whole_origin(); + *missing.status_mut() = StatusCode::NOT_FOUND; + let delivered = futures::executor::block_on(claim.deliver_whole( + &services, + &identity(), + missing, + part_origin(), + )); + let (response, body) = split(delivered); + assert_eq!( + response.status(), + StatusCode::PARTIAL_CONTENT, + "should keep the origin's part" + ); + assert_eq!(body, SOURCE[0..=3]); + assert_eq!( + header(&response, "etag"), + None, + "an unsigned fallback carries no validator to pin it" + ); + assert_eq!( + header(&response, "cache-control"), + Some("private, max-age=60") + ); + assert!(signer.jobs().is_empty()); +} + +#[test] +fn answers_head_from_the_signed_whole_with_no_body() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + let claim = claim(&services, &Method::HEAD, &image_request(&[])); + let head = origin_response( + StatusCode::OK, + EdgeBody::empty(), + &[(header::CONTENT_LENGTH, "18")], + ); + + let Delivery::FetchWhole(fallback) = delivery(&claim, &services, head) else { + panic!("a HEAD of a bounded type that can be kept should ask for the whole body"); + }; + let delivered = futures::executor::block_on(claim.deliver_whole( + &services, + &identity(), + whole_origin(), + fallback, + )); + let (response, body) = split(delivered); + assert_eq!(response.status(), StatusCode::OK); + assert!(body.is_empty(), "a HEAD carries no body"); + assert_eq!( + header(&response, "content-length"), + Some(signed_bytes().len().to_string().as_str()), + "a HEAD advertises the signed length" + ); + assert_eq!(header(&response, "etag"), Some(signed_etag().as_str())); +} + +#[test] +fn never_signs_a_response_the_origin_marked_as_one_readers() { + let private_markers: [(HeaderName, &str); 4] = [ + (header::SET_COOKIE, "session=1"), + (header::CACHE_CONTROL, "private, max-age=60"), + (header::VARY, "Accept, Cookie"), + (header::VARY, "*"), + ]; + for (name, value) in private_markers { + let signer = StubSigner::new(Behavior::Sign, 1024); + let mut response = whole_origin(); + response + .headers_mut() + .insert(name.clone(), HeaderValue::from_static(value)); + let (response, body) = deliver(&signer, &Method::GET, &HeaderMap::new(), response); + assert!( + signer.jobs().is_empty(), + "{name}: {value} should not reach the signer" + ); + assert_eq!( + body, SOURCE, + "{name}: {value} should go out as the origin sent it" + ); + assert_eq!(header(&response, "etag"), Some(ORIGIN_ETAG)); + } +} + +#[test] +fn reads_the_origins_own_headers_not_the_policy_core_wrote_over_them() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = signer.installed_in(noop_services()); + let claim = claim(&services, &Method::GET, &HeaderMap::new()); + let raw = origin_response( + StatusCode::OK, + EdgeBody::empty(), + &[(header::CACHE_CONTROL, "public, max-age=60, no-transform")], + ); + let facts = OriginFacts::of(raw.headers()); + let normalized = origin_response( + StatusCode::OK, + EdgeBody::from(SOURCE.to_vec()), + &[(header::CACHE_CONTROL, "public, max-age=31536000, immutable")], + ); + + futures::executor::block_on(claim.deliver( + &services, + &identity(), + Answered::Origin(facts), + normalized, + )); + assert!( + signer.jobs().is_empty(), + "the origin's no-transform stands under core's policy" + ); +} + +#[test] +fn claims_nothing_without_a_signer_with_authorization_or_off_its_paths() { + let settings = create_test_settings(); + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = signer.installed_in(noop_services()); + let none = |services: &RuntimeServices, method: &Method, path: &str, headers: &HeaderMap| { + MediaClaim::of_request(&settings, services, method, path, headers, true).is_none() + }; + assert!( + none(&noop_services(), &Method::GET, PATH, &HeaderMap::new()), + "services without a signer claim nothing" + ); + assert!( + none( + &services, + &Method::GET, + PATH, + &request_headers(&[(header::AUTHORIZATION, "Bearer reader-token")]) + ), + "a request bound to one reader's credentials is never claimed" + ); + assert!( + none(&services, &Method::GET, "/api/items", &HeaderMap::new()), + "a path the signer leaves alone" + ); + assert!( + none(&services, &Method::POST, PATH, &HeaderMap::new()), + "only GET and HEAD are claimed" + ); +} + +#[test] +fn tells_a_media_request_by_destination_accept_or_extension() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = signer.installed_in(noop_services()); + let settings = create_test_settings(); + let looks = |path: &str, headers: &HeaderMap| { + MediaClaim::of_request(&settings, &services, &Method::GET, path, headers, true) + .expect("should claim") + .looks_like_media() + }; + assert!( + looks(PATH, &image_request(&[])), + "an image destination is media" + ); + assert!(!looks( + "/photo.jpg", + &request_headers(&[("sec-fetch-dest".parse().expect("should parse"), "script")]) + )); + assert!(looks( + PATH, + &request_headers(&[(header::ACCEPT, "image/avif,image/webp,*/*")]) + )); + assert!( + looks("/clips/intro.MP4", &HeaderMap::new()), + "a media extension is media" + ); + assert!( + !looks(PATH, &request_headers(&[(header::ACCEPT, "text/html,*/*")])), + "a page is not" + ); +} + +#[test] +fn leaves_untransformable_encoded_and_unhandled_bodies_as_the_origin_sent_them() { + let extras: [(HeaderName, &str); 3] = [ + (header::CACHE_CONTROL, "public, no-transform"), + (header::CONTENT_ENCODING, "gzip"), + (header::CONTENT_TYPE, "text/plain"), + ]; + for (name, value) in extras { + let signer = StubSigner::new(Behavior::Sign, 1024); + let mut response = whole_origin(); + response + .headers_mut() + .insert(name.clone(), HeaderValue::from_static(value)); + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::RANGE, "bytes=0-1")]), + response, + ); + assert!( + signer.jobs().is_empty(), + "{name}: {value} should not reach the signer" + ); + assert_eq!( + response.status(), + StatusCode::OK, + "{name}: {value} keeps the origin's status" + ); + assert_eq!(body, SOURCE); + } +} + +#[test] +fn passes_other_statuses_through_untouched() { + for status in [ + StatusCode::NOT_FOUND, + StatusCode::NOT_MODIFIED, + StatusCode::RANGE_NOT_SATISFIABLE, + ] { + let signer = StubSigner::new(Behavior::Sign, 1024); + let response = origin_response(status, EdgeBody::empty(), &[]); + let (response, _) = deliver(&signer, &Method::GET, &HeaderMap::new(), response); + assert_eq!( + response.status(), + status, + "a reader that asked no range gets the origin's {status}" + ); + assert_eq!(header(&response, "etag"), Some(ORIGIN_ETAG)); + assert!(signer.jobs().is_empty()); + } +} + +#[test] +fn sends_the_origin_bytes_unsigned_without_validators_when_signing_fails() { + for behavior in [Behavior::Fail, Behavior::ReservedHeader] { + let signer = StubSigner::new(behavior, 1024); + let (response, body) = deliver( + &signer, + &Method::GET, + &request_headers(&[(header::RANGE, "bytes=0-5")]), + whole_origin(), + ); + assert_eq!( + signer.jobs().len(), + 1, + "{behavior:?} should have asked the signer" + ); + assert_eq!( + response.status(), + StatusCode::OK, + "{behavior:?} keeps the origin's own answer" + ); + assert_eq!(body, SOURCE); + assert_eq!( + header(&response, "etag"), + None, + "{behavior:?} drops the origin's validator" + ); + assert_eq!( + header(&response, "cache-control"), + Some("private, max-age=60") + ); + assert_eq!(header(&response, "x-test-manifest"), None); + assert_eq!(header(&response, "repr-digest"), None); + } +} + +#[test] +fn captures_a_streamed_body_within_the_limit() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let chunks = SOURCE + .chunks(5) + .map(Bytes::copy_from_slice) + .collect::>(); + let (response, body) = deliver( + &signer, + &Method::GET, + &HeaderMap::new(), + origin_response( + StatusCode::OK, + EdgeBody::stream(futures::stream::iter(chunks)), + &[], + ), + ); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + signer.jobs()[0].source, + SOURCE, + "should join the streamed chunks exactly" + ); + assert_eq!(body, signed_bytes()); +} + +#[test] +fn streams_a_body_over_the_limit_unsigned_and_whole() { + let signer = StubSigner::new(Behavior::Sign, 8); + let chunks = SOURCE + .chunks(4) + .map(Bytes::copy_from_slice) + .collect::>(); + let (response, body) = deliver( + &signer, + &Method::GET, + &HeaderMap::new(), + origin_response( + StatusCode::OK, + EdgeBody::stream(futures::stream::iter(chunks)), + &[], + ), + ); + assert!( + signer.jobs().is_empty(), + "an oversized body should not reach the signer" + ); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + body, SOURCE, + "should send every byte read and unread, in order" + ); + assert_eq!(header(&response, "etag"), Some(ORIGIN_ETAG)); +} + +#[test] +fn keeps_a_failed_stream_failing_after_the_bytes_read() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = signer.installed_in(noop_services()); + let claim = claim(&services, &Method::GET, &HeaderMap::new()); + let stream = futures::stream::iter(vec![ + Ok(Bytes::from_static(b"first")), + Err(std::io::Error::other("origin reset")), + ]); + let Delivery::Send(delivered) = delivery( + &claim, + &services, + origin_response(StatusCode::OK, EdgeBody::from_stream(stream), &[]), + ) else { + panic!("should send the origin's answer"); + }; + let EdgeBody::Stream(mut body) = delivered.into_body() else { + panic!("should keep a failed body streaming"); + }; + futures::executor::block_on(async { + let first = body.next().await.expect("should yield the bytes read"); + assert_eq!( + first.expect("should read the first chunk"), + Bytes::from_static(b"first") + ); + assert!( + body.next() + .await + .expect("should yield the failure") + .is_err(), + "should hand the reader the origin's failure, never a short body as whole" + ); + }); + assert!( + signer.jobs().is_empty(), + "a body that failed is never signed" + ); +} + +/// Signs once through `services` for a reader whose request is `headers`, +/// then gives back what a later image request finds before the origin. +fn sign_then_look_up( + signer: &Arc, + services: &RuntimeServices, + headers: &HeaderMap, + shared: bool, + origin: Response, +) -> Option> { + let first = claim_shared(services, &Method::GET, headers, shared); + let _ = delivery(&first, services, origin); + assert_eq!(signer.jobs().len(), 1, "should sign the first request"); + let later = claim(services, &Method::GET, &image_request(&[])); + futures::executor::block_on(later.kept(services, &identity())) +} + +#[test] +fn answers_a_later_range_from_the_kept_signed_representation_before_the_origin() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + + let kept = sign_then_look_up( + &signer, + &services, + &image_request(&[]), + true, + whole_origin(), + ) + .expect("should keep a fresh signed representation"); + let age = header(&kept, "age") + .and_then(|age| age.parse::().ok()) + .expect("a kept copy carries its age"); + assert!( + age <= 1, + "a copy kept just now has almost no age, got {age}" + ); + + let ranged = image_request(&[(header::RANGE, "bytes=1-4")]); + let claim = claim(&services, &Method::GET, &ranged); + let Delivery::Send(response) = + futures::executor::block_on(claim.deliver(&services, &identity(), Answered::Kept, kept)) + else { + panic!("a kept representation is answered at once"); + }; + let (response, body) = split(response); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!(body, signed_bytes()[1..=4]); + assert_eq!(header(&response, "etag"), Some(signed_etag().as_str())); + assert_eq!(header(&response, "x-test-manifest"), Some("instance-1")); + assert_eq!(signer.jobs().len(), 1, "the kept copy is not signed again"); +} + +#[test] +fn keeps_and_serves_nothing_for_a_request_whose_answer_could_be_one_readers() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + assert!( + sign_then_look_up( + &signer, + &services, + &image_request(&[]), + false, + whole_origin() + ) + .is_none(), + "an answer to a cookie-bearing request is never kept for other readers" + ); + + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + assert!( + sign_then_look_up( + &signer, + &services, + &image_request(&[]), + true, + whole_origin() + ) + .is_some() + ); + let cookie_bearing = claim_shared(&services, &Method::GET, &image_request(&[]), false); + assert!( + futures::executor::block_on(cookie_bearing.kept(&services, &identity())).is_none(), + "a cookie-bearing request is never answered from a copy kept for others" + ); + let revalidating = claim( + &services, + &Method::GET, + &image_request(&[(header::CACHE_CONTROL, "no-cache")]), + ); + assert!( + futures::executor::block_on(revalidating.kept(&services, &identity())).is_none(), + "a request asking for revalidation is not answered from a kept copy" + ); + let not_media = claim(&services, &Method::GET, &HeaderMap::new()); + assert!( + futures::executor::block_on(not_media.kept(&services, &identity())).is_none(), + "a request that does not look like media is not looked up" + ); +} + +#[test] +fn serves_nothing_kept_once_the_signers_namespace_changes_or_fails() { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + assert!( + sign_then_look_up( + &signer, + &services, + &image_request(&[]), + true, + whole_origin() + ) + .is_some() + ); + let claim = claim(&services, &Method::GET, &image_request(&[])); + + signer.set_namespace(Ok("policy-2".to_owned())); + assert!( + futures::executor::block_on(claim.kept(&services, &identity())).is_none(), + "a changed policy finds nothing kept under the old one" + ); + + signer.set_namespace(Err(MediaSigningFailure { reason: "revoked" })); + assert!( + futures::executor::block_on(claim.kept(&services, &identity())).is_none(), + "a revoked signer serves nothing kept" + ); +} + +#[test] +fn keeps_nothing_the_origin_did_not_make_shareably_fresh() { + for cache_control in ["no-cache", "public", "public, max-age=0"] { + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + let mut origin = whole_origin(); + origin.headers_mut().insert( + header::CACHE_CONTROL, + HeaderValue::from_static(cache_control), + ); + assert!( + sign_then_look_up(&signer, &services, &image_request(&[]), true, origin).is_none(), + "`{cache_control}` should keep no signed copy" + ); + } + + let signer = StubSigner::new(Behavior::Sign, 1024); + let services = keeping(&signer); + let mut origin = whole_origin(); + origin + .headers_mut() + .insert(header::VARY, HeaderValue::from_static("Accept")); + assert!( + sign_then_look_up(&signer, &services, &image_request(&[]), true, origin).is_none(), + "an answer that varies on the reader's Accept keeps no single signed copy" + ); +} + +#[test] +fn a_kept_copy_ages_and_is_refused_when_its_body_is_not_the_one_signed() { + let mut headers = HeaderMap::new(); + let digest = Sha256::digest(signed_bytes()); + headers.insert( + header::ETAG, + HeaderValue::from_str(&signed_entity_tag(&digest)).expect("should build tag"), + ); + let body = Bytes::from(signed_bytes()); + let encoded = encode_kept(&KeptRecord { + kept_at: 1_000, + age: 5, + headers: &headers, + body: &body, + }); + + let kept = decode_kept(&encoded, 1_030).expect("should read an intact copy"); + assert_eq!( + header(&kept, "age"), + Some("35"), + "should add the time kept to the origin's age" + ); + + let mut altered = encoded.to_vec(); + let last = altered.len() - 1; + altered[last] ^= 0x01; + assert!( + decode_kept(&Bytes::from(altered), 1_030).is_none(), + "a body that no longer hashes to its tag is never served" + ); +} + +#[test] +fn strips_every_header_that_shortens_the_origin_answer() { + let mut headers = request_headers(&[ + (header::RANGE, "bytes=0-1"), + (header::IF_RANGE, ORIGIN_ETAG), + (header::IF_NONE_MATCH, ORIGIN_ETAG), + (header::IF_MODIFIED_SINCE, "Tue, 01 Sep 2026 00:00:00 GMT"), + (header::IF_MATCH, ORIGIN_ETAG), + (header::IF_UNMODIFIED_SINCE, "Tue, 01 Sep 2026 00:00:00 GMT"), + (header::ACCEPT, "image/avif"), + ]); + strip_representation_preconditions(&mut headers); + assert_eq!(headers.len(), 1, "should keep only the unrelated header"); + assert!(headers.contains_key(header::ACCEPT)); +} + +/// A manifest the origin already links its bytes to. +const ORIGIN_MANIFEST_LINK: &str = "; rel=\"c2pa-manifest\""; + +/// `response` with the origin's own manifest `Link`. +fn linked(mut response: Response) -> Response { + response + .headers_mut() + .append(header::LINK, HeaderValue::from_static(ORIGIN_MANIFEST_LINK)); + response +} + +fn links(response: &Response) -> Vec { + response + .headers() + .get_all(header::LINK) + .iter() + .map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned()) + .collect() +} + +#[test] +fn keeps_an_origin_manifest_link_and_its_bytes_for_external_and_embeddable_signers() { + // `SignWithLink` stands for an external-only signer, `Sign` for one + // that embeds a manifest and changes the bytes. + for behavior in [Behavior::SignWithLink, Behavior::Sign] { + let signer = StubSigner::new(behavior, 1024); + let (response, body) = deliver( + &signer, + &Method::GET, + &image_request(&[]), + linked(whole_origin()), + ); + assert!( + signer.jobs().is_empty(), + "{behavior:?}: should never sign bytes that name a manifest of their own" + ); + assert_eq!(body, SOURCE, "{behavior:?}: should send the origin's bytes"); + assert_eq!( + header(&response, "etag"), + Some(ORIGIN_ETAG), + "{behavior:?}: should keep the origin's validator" + ); + assert_eq!( + links(&response), + vec![ORIGIN_MANIFEST_LINK.to_owned()], + "{behavior:?}: should keep only the origin's manifest link" + ); + assert_eq!(header(&response, "x-test-manifest"), None); + assert_eq!(header(&response, "repr-digest"), None); + } +} + +#[test] +fn never_signs_a_whole_fetch_where_an_answer_names_an_origin_manifest() { + let signer = StubSigner::new(Behavior::SignWithLink, 1024); + let services = keeping(&signer); + let first_claim = claim( + &services, + &Method::GET, + &image_request(&[(header::RANGE, "bytes=0-3")]), + ); + let Delivery::Send(response) = delivery(&first_claim, &services, linked(part_origin())) else { + panic!("should never ask for the whole behind a part that names an origin manifest"); + }; + let (response, body) = split(response); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!(body, SOURCE[0..=3]); + assert_eq!(links(&response), vec![ORIGIN_MANIFEST_LINK.to_owned()]); + assert!(signer.jobs().is_empty()); + + let tail = signed_bytes().len() - 2; + let range = format!("bytes={tail}-"); + for (refusal_linked, whole_linked) in [(false, true), (true, false)] { + let signer = StubSigner::new(Behavior::SignWithLink, 1024); + let services = signer.installed_in(noop_services()); + let claim = claim( + &services, + &Method::GET, + &request_headers(&[(header::RANGE, range.as_str())]), + ); + let refusal = origin_response( + StatusCode::RANGE_NOT_SATISFIABLE, + EdgeBody::empty(), + &[(header::CONTENT_RANGE, "bytes */18")], + ); + let refusal = if refusal_linked { + linked(refusal) + } else { + refusal + }; + let Delivery::FetchWhole(fallback) = delivery(&claim, &services, refusal) else { + panic!("a range past the origin's length may name the signed tail"); + }; + let whole = if whole_linked { + linked(whole_origin()) + } else { + whole_origin() + }; + let delivered = futures::executor::block_on(claim.deliver_whole( + &services, + &identity(), + whole, + fallback, + )); + let (response, body) = split(delivered); + let case = format!("refusal_linked={refusal_linked} whole_linked={whole_linked}"); + assert!(signer.jobs().is_empty(), "{case}: should sign nothing"); + assert_eq!( + response.status(), + StatusCode::RANGE_NOT_SATISFIABLE, + "{case}: should send the origin's own answer" + ); + assert!(body.is_empty(), "{case}"); + assert_eq!(header(&response, "etag"), Some(ORIGIN_ETAG), "{case}"); + let expected = if refusal_linked { + vec![ORIGIN_MANIFEST_LINK.to_owned()] + } else { + Vec::new() + }; + assert_eq!( + links(&response), + expected, + "{case}: should add no manifest link" + ); + } +} + +/// One ISO BMFF box of `kind` around `body`, with a 32-bit size. +fn bmff_box(kind: &[u8; 4], body: &[u8]) -> Vec { + let size = u32::try_from(body.len() + 8).expect("should fit a 32-bit size"); + let mut bytes = size.to_be_bytes().to_vec(); + bytes.extend_from_slice(kind); + bytes.extend_from_slice(body); + bytes +} + +fn concat(parts: &[Vec]) -> Vec { + parts.concat() +} + +/// A C2PA verifiable segment info `emsg`, as a packager writes it. +fn c2pa_emsg() -> Vec { + bmff_box(b"emsg", b"\0\0\0\0urn:c2pa:verifiable-segment-info\0fseg\0") +} + +#[test] +fn tells_the_parts_of_a_fragmented_stream_from_a_progressive_file() { + let init = concat(&[ + bmff_box(b"ftyp", b"cmfc\0\0\0\0iso6cmfc"), + bmff_box( + b"moov", + &concat(&[ + bmff_box(b"mvhd", &[0; 100]), + bmff_box(b"trak", &bmff_box(b"tkhd", &[0; 84])), + bmff_box(b"mvex", &bmff_box(b"trex", &[0; 24])), + bmff_box(b"pssh", &[0; 32]), + ]), + ), + ]); + let fragment = || { + vec![ + bmff_box(b"sidx", &[0; 32]), + bmff_box(b"moof", &bmff_box(b"mfhd", &[0; 8])), + bmff_box(b"mdat", b"coded-samples"), + ] + }; + let segment = concat(&[vec![bmff_box(b"styp", b"msdh\0\0\0\0msdhmsix")], fragment()].concat()); + let segment_with_emsg = concat( + &[ + vec![bmff_box(b"styp", b"msdh\0\0\0\0msdhmsix"), c2pa_emsg()], + fragment(), + ] + .concat(), + ); + let chunk_without_styp = concat(&fragment()); + let progressive = concat(&[ + bmff_box(b"ftyp", b"isom\0\0\0\0isomavc1"), + bmff_box(b"moov", &bmff_box(b"trak", &[0; 16])), + bmff_box(b"mdat", b"coded-samples"), + ]); + let fragmented_file = concat(&[ + bmff_box(b"ftyp", b"iso6\0\0\0\0iso6dash"), + bmff_box(b"moov", &bmff_box(b"mvex", &[0; 8])), + bmff_box(b"moof", &[0; 8]), + bmff_box(b"mdat", b"coded-samples"), + ]); + let mut large_size = init.clone(); + // The same init with its `ftyp` given a 64-bit size. + let ftyp_body = b"cmfc\0\0\0\0iso6cmfc"; + let mut large_ftyp = 1_u32.to_be_bytes().to_vec(); + large_ftyp.extend_from_slice(b"ftyp"); + large_ftyp.extend_from_slice(&(16 + ftyp_body.len() as u64).to_be_bytes()); + large_ftyp.extend_from_slice(ftyp_body); + large_size.splice(0..8 + ftyp_body.len(), large_ftyp); + let mut truncated = segment.clone(); + truncated.truncate(segment.len() - 3); + let mut zero_sized_last = segment.clone(); + let mdat_at = zero_sized_last.len() - (8 + b"coded-samples".len()); + zero_sized_last[mdat_at..mdat_at + 4].copy_from_slice(&0_u32.to_be_bytes()); + let too_many = bmff_box(b"free", &[]).repeat(MAX_BMFF_BOXES + 1); + + for (case, bytes, expected) in [ + ("Shaka-style init", &init, true), + ("init with a 64-bit ftyp size", &large_size, true), + ("media segment", &segment, true), + ("media segment with a C2PA emsg", &segment_with_emsg, true), + ("CMAF chunk without styp", &chunk_without_styp, true), + ("segment whose mdat runs to the end", &zero_sized_last, true), + ("progressive file", &progressive, false), + ("fragmented file with its media", &fragmented_file, false), + ("truncated segment", &truncated, false), + ("too many boxes", &too_many, false), + ("an image", &SOURCE.to_vec(), false), + ("nothing", &Vec::new(), false), + ] { + assert_eq!( + is_fragmented_stream_part(bytes), + expected, + "{case}: should be told correctly" + ); + } +} + +#[test] +fn origin_freshness_keeps_browser_and_shared_age_and_processing_independent() { + let receipt_seconds = 1_700_000_000; + let receipt = UNIX_EPOCH + Duration::from_secs(receipt_seconds); + for (control, age, date_age, elapsed, browser, shared) in [ + ( + "public, max-age=60, s-maxage=3600", + Some("0"), + None, + 0, + 60, + 3600, + ), + ( + "public, max-age=60, s-maxage=3600", + Some("5"), + Some(120), + 0, + 0, + 3480, + ), + ( + "public, max-age=60, s-maxage=3600", + None, + Some(120), + 0, + 0, + 3480, + ), + ( + "public, max-age=60, s-maxage=3600", + Some("200"), + Some(120), + 0, + 0, + 3400, + ), + ( + "public, max-age=90, s-maxage=180", + Some("10"), + None, + 40, + 40, + 130, + ), + ( + "public, max-age=90, s-maxage=180", + Some("10"), + None, + 90, + 0, + 80, + ), + ( + "public, max-age=90, s-maxage=10", + Some("15"), + None, + 0, + 75, + 0, + ), + ("public, max-age=90", Some("70"), None, 0, 20, 20), + ("public, max-age=90", Some("100"), None, 0, 0, 0), + ("public, s-maxage=90", None, None, 0, 0, 90), + ("public, max-age=bad, s-maxage=90", None, None, 0, 0, 90), + ("public, max-age=60, s-maxage=bad", None, None, 0, 60, 0), + ("public, max-age=60, s-maxage", None, None, 0, 60, 0), + ("public, max-age=60, max-age=90", None, None, 0, 0, 0), + ("public, max-age=60", Some("bad"), None, 0, 0, 0), + ("no-cache, max-age=90", Some("0"), None, 0, 0, 0), + ("public", Some("0"), None, 0, 0, 0), + ] { + let mut headers = HeaderMap::new(); + headers.insert( + header::CACHE_CONTROL, + HeaderValue::from_str(control).expect("control"), + ); + if let Some(age) = age { + headers.insert(header::AGE, HeaderValue::from_str(age).expect("age")); + } + if let Some(date_age) = date_age { + let date = httpdate::fmt_http_date( + std::time::UNIX_EPOCH + Duration::from_secs(receipt_seconds - date_age), + ); + headers.insert(header::DATE, HeaderValue::from_str(&date).expect("date")); + } + assert_eq!( + OriginFacts::of_at(&headers, receipt).remaining_freshness(Duration::from_secs(elapsed)), + OriginFreshness { + browser: Duration::from_secs(browser), + shared: Duration::from_secs(shared), + }, + "{control}, Age {age:?}, apparent age {date_age:?}, processing {elapsed}", + ); + } +} + +#[test] +fn invalid_origin_date_cannot_gain_freshness_when_rebased() { + let mut headers = HeaderMap::new(); + headers.insert( + header::CACHE_CONTROL, + HeaderValue::from_static("max-age=3600"), + ); + headers.insert(header::DATE, HeaderValue::from_static("not a date")); + assert_eq!( + OriginFacts::of_at(&headers, UNIX_EPOCH).remaining_freshness(Duration::ZERO), + OriginFreshness::default(), + ); +} diff --git a/crates/trusted-server-core/src/middleware.rs b/crates/trusted-server-core/src/middleware.rs index c686bd4336..35bea5ff7a 100644 --- a/crates/trusted-server-core/src/middleware.rs +++ b/crates/trusted-server-core/src/middleware.rs @@ -46,7 +46,7 @@ use std::rc::Rc; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use std::time::Duration; -use http::{HeaderName, HeaderValue, StatusCode}; +use http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use crate::integrations::{AttributeRewriteAction, IntegrationDocumentState, ScriptRewriteAction}; @@ -59,6 +59,17 @@ use crate::streaming_processor::StreamProcessor; /// unless it says it handles another. pub const HTML_MEDIA_TYPE: &str = "text/html"; +/// A module's exact original byte ranges that no document transform may edit. +/// Called on the complete, bounded document before core or middleware runs. +/// Each classifier returns ordered, disjoint, nonempty ranges inside the input. +/// An error refuses the document. Core treats ranges as opaque; classifications +/// from different middleware are combined, including overlapping ranges. +/// +/// Transforms see an opaque comment in place of each protected range. A +/// middleware that signs or hashes raw whole HTML must not bind that intermediate +/// view and assume its binding remains valid after original bytes are restored. +pub type SourceProtection = fn(&[u8]) -> Result>, std::io::Error>; + /// Whether a middleware may run on a response of `media_type`, written as a /// bare `type/subtype` in lower case. /// @@ -144,7 +155,28 @@ pub struct MiddlewareContext<'a> { /// sends, the same for a page served from the store as for one fetched /// for this request. pub origin_host: &'a str, - /// What the adapter reports about the platform the deployment runs on. + /// The status of the response the document is the body of. A page from + /// the store of shared templates was stored from a `200`. + pub status: StatusCode, + /// Whether the origin marked its response as one reader's, by setting a + /// cookie or by a `Cache-Control` that forbids shared storage. Read from + /// the origin's own headers, before core writes its cache policy on the + /// response, so a middleware that sends the document elsewhere can leave + /// a reader's own page alone. A page from the store of shared templates + /// was never one reader's. + pub origin_private: bool, + /// The headers of the reader's response as core will send them, being + /// the origin's after core moved its addresses to the reader's host, for + /// a middleware whose change has to respect a header such as + /// `Content-Security-Policy`. `None` where core cannot say, being the + /// fetch phase of a page that may be stored and a page replayed from the + /// store, which a middleware treats as headers it does not know. A page + /// replayed from the store is never one whose origin forbade transforming + /// it or named a C2PA manifest in its `Link`: core keeps such a page out of + /// the store, so it is always read with its origin's own headers. + pub response_headers: Option<&'a HeaderMap>, + /// What the adapter reports about the platform the deployment runs on, + /// as the attestation evidence carries it. pub platform: &'a PlatformIdentity, /// State shared by the middleware working on this document in this /// phase. @@ -556,6 +588,22 @@ pub trait Middleware: Send + Sync { false } + /// Runs its stream processor after every ordinary processor in this chain. + /// + /// Use this for a seal or signature that must cover the final transformed + /// document. At most one middleware may produce a finalizing processor + /// for a document; handlers and inserts still run before all processors. + fn finalizes_document(&self) -> bool { + false + } + + /// Classifies source bytes that must survive all document transformations + /// unchanged. Default off. Only middleware in the applicable chains may + /// request this hold, including serve middleware before a template is built. + fn source_protection(&self) -> Option { + None + } + /// Whether this middleware moves the origin's address to the reader's /// host itself, in a body of a media type it handles other than HTML. /// Core then leaves the addresses in such a body alone, whether or not @@ -916,6 +964,15 @@ impl MiddlewareChain { self.middleware.is_empty() } + /// The original-source classifiers selected by this document's chain. + #[must_use] + pub fn source_protections(&self) -> Vec { + self.middleware + .iter() + .filter_map(|middleware| middleware.source_protection()) + .collect() + } + /// Whether a middleware in the chain writes the whole document, see /// [`Middleware::writes_whole_document`]. #[must_use] @@ -958,13 +1015,16 @@ impl MiddlewareChain { /// Checks each handler's selector, refusing the document when one does /// not parse. On a media type other than HTML, an action that decides /// about HTML is refused the same way. + /// Ordinary stream processors keep their declared order. One document + /// finalizer runs last, after every other processor and HTML edit. /// /// # Errors /// - /// A message naming the middleware and the selector that does not parse, - /// or the HTML decision it made about a document that is not HTML. + /// A message naming an invalid selector, an HTML decision on a non-HTML + /// document, an inadmissible response header, or competing finalizers. pub fn plan(&self, context: &MiddlewareContext<'_>) -> Result { let mut plan = MiddlewarePlan::default(); + let mut final_processor = None; for middleware in &self.middleware { let action = middleware.create(context); action.validate_selectors().map_err(|problem| { @@ -998,9 +1058,25 @@ impl MiddlewareChain { plan.body_open_inserts.extend(action.body_open_inserts); plan.element_handlers.extend(action.element_handlers); plan.text_handlers.extend(action.text_handlers); - plan.processors.extend(action.stream); + if let Some(processor) = action.stream { + if middleware.finalizes_document() { + if let Some((other, _)) = &final_processor { + return Err(format!( + "the {} middleware `{}` and `{other}` both finalize the document", + self.phase, + middleware.middleware_id() + )); + } + final_processor = Some((middleware.middleware_id(), processor)); + } else { + plan.processors.push(processor); + } + } plan.response_headers.extend(action.response_headers); } + if let Some((_, processor)) = final_processor { + plan.processors.push(processor); + } Ok(plan) } } @@ -1023,6 +1099,7 @@ pub struct MiddlewarePlan { /// Text handlers, in the order the middleware were named. pub text_handlers: Vec>, /// Stream processors, each handed what the one before it produced. + /// Ordinary processors keep their declared order; a finalizer runs last. pub processors: Vec>, /// Headers for the reader's response, in the order the middleware were /// named, a later one replacing an earlier of the same name. @@ -1102,6 +1179,9 @@ pub mod test_support { request_host: REQUEST_HOST, request_scheme: "https", origin_host: ORIGIN_HOST, + status: http::StatusCode::OK, + origin_private: false, + response_headers: None, platform, document_state, max_buffered_script_bytes: 16 * 1024 * 1024, diff --git a/crates/trusted-server-core/src/middleware/tests.rs b/crates/trusted-server-core/src/middleware/tests.rs index 72814b6d26..6e6474912c 100644 --- a/crates/trusted-server-core/src/middleware/tests.rs +++ b/crates/trusted-server-core/src/middleware/tests.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use serde_json::json; +use sha2::{Digest, Sha256}; use super::*; use crate::html_processor::{ @@ -67,6 +68,9 @@ fn reader(request_state: &IntegrationRequestState) -> ReaderDocument<'_> { request_host: REQUEST_HOST, request_scheme: "https", origin_host: ORIGIN_HOST, + status: StatusCode::OK, + origin_private: false, + response_headers: None, platform: test_support::platform(), request_state, max_buffered_script_bytes: 16 * 1024 * 1024, @@ -1608,6 +1612,86 @@ fn a_serve_chain_s_stream_processor_runs_over_the_reader_s_copy() { assert!(page.contains("Reader") && page.ends_with("")); } +struct DocumentSeal(Sha256); + +impl crate::streaming_processor::StreamProcessor for DocumentSeal { + fn process_chunk(&mut self, chunk: &[u8], is_last: bool) -> io::Result> { + self.0.update(chunk); + let mut output = chunk.to_vec(); + if is_last { + output.extend_from_slice( + format!("\nsha256:{}", hex::encode(self.0.finalize_reset())).as_bytes(), + ); + } + Ok(output) + } +} + +struct SealingMiddleware(&'static str); + +impl Middleware for SealingMiddleware { + fn middleware_id(&self) -> &'static str { + self.0 + } + + fn phases(&self) -> &[MiddlewarePhase] { + &[MiddlewarePhase::Serve] + } + + fn finalizes_document(&self) -> bool { + true + } + + fn create(&self, _context: &MiddlewareContext<'_>) -> MiddlewareAction { + MiddlewareAction { + stream: Some(Box::new(DocumentSeal(Sha256::new()))), + ..MiddlewareAction::pass() + } + } +} + +#[test] +fn finalizing_document_sees_every_transform_even_when_registered_first() { + let chain = serve_chain_of(vec![ + Arc::new(SealingMiddleware("example.seal")), + middleware("example.first", |_| MiddlewareAction { + stream: Some(Box::new(Append("\nfirst".to_owned()))), + ..MiddlewareAction::pass() + }), + middleware("example.second", |_| MiddlewareAction { + stream: Some(Box::new(Append("\nsecond".to_owned()))), + ..MiddlewareAction::pass() + }), + head("example.head", ""), + ]); + for chunk_size in [1, 8192] { + let output = served_with( + &chain, + &IntegrationRequestState::default(), + "Article", + chunk_size, + ); + let (document, seal) = output + .rsplit_once("\nsha256:") + .expect("should carry a seal"); + assert_eq!(seal, hex::encode(Sha256::digest(document.as_bytes()))); + assert!(document.ends_with("\nfirst\nsecond")); + assert!(document.contains("")); + } +} + +#[test] +fn competing_finalizing_documents_are_refused_before_delivery() { + let chain = serve_chain_of(vec![ + Arc::new(SealingMiddleware("example.first-seal")), + Arc::new(SealingMiddleware("example.second-seal")), + ]); + assert!( + create_serve_processor(&chain, &reader(&IntegrationRequestState::default())).is_err(), + "two finalizers must not produce a document whose first seal is invalid" + ); +} + #[test] fn a_serve_middleware_reads_what_the_request_left() { let chain = serve_chain_of(vec![middleware("example.reads", |context| { @@ -2388,3 +2472,119 @@ fn an_html_entry_ahead_does_not_shadow_the_entry_that_makes_the_file() { "should make the file from the text/plain entry, not as HTML" ); } + +/// A format-agnostic source policy supplied by a test module. Core knows only +/// its returned byte range, not why that subtree must remain unchanged. +struct ProtectedFixture; + +const PROTECTED_FIXTURE: &str = "

keep

keep

"; + +impl Middleware for ProtectedFixture { + fn middleware_id(&self) -> &'static str { + "fixture.protected" + } + fn phases(&self) -> &[MiddlewarePhase] { + &MiddlewarePhase::ALL + } + fn create(&self, _: &MiddlewareContext<'_>) -> MiddlewareAction { + MiddlewareAction::pass() + } + fn source_protection(&self) -> Option { + Some(|original| { + Ok(original + .windows(PROTECTED_FIXTURE.len()) + .position(|bytes| bytes == PROTECTED_FIXTURE.as_bytes()) + .map(|start| start..start + PROTECTED_FIXTURE.len()) + .into_iter() + .collect()) + }) + } +} + +#[test] +fn source_policy_protects_before_core_fetch_and_warm_serve_descendant_handlers() { + use crate::html_processor::create_html_processor_with_source_protection; + + let fetch = chain_of(vec![ + element("fixture.fetch-attribute", "a.edit", "href", |_| { + AttributeRewriteAction::Replace("fetch-edited".to_owned()) + }), + text("fixture.fetch-text", "p.edit", || { + |_, last| { + if last { + ScriptRewriteAction::Replace("fetch-edited".to_owned()) + } else { + ScriptRewriteAction::remove_node() + } + } + }), + ]); + let serve = serve_chain_of(vec![ + Arc::new(ProtectedFixture), + element("fixture.serve-attribute", "a.edit", "href", |_| { + AttributeRewriteAction::Replace("serve-edited".to_owned()) + }), + text("fixture.serve-text", "p.edit", || { + |_, last| { + if last { + ScriptRewriteAction::Replace("serve-edited".to_owned()) + } else { + ScriptRewriteAction::remove_node() + } + } + }), + ]); + let html = format!( + "{PROTECTED_FIXTURE}\ + outside\ +

outside

" + ); + // The source policy lives in serve, but must already cover fetch/template + // construction, without running any of serve's actions early. + let (mut processor, _) = + create_html_processor_with_source_protection(config(), &fetch, None, &[&fetch, &serve]) + .expect("plan template"); + for chunk in html.as_bytes().chunks(3) { + assert!( + processor + .process_chunk(chunk, false) + .expect("hold original") + .is_empty() + ); + } + let template = String::from_utf8(processor.process_chunk(&[], true).expect("finish template")) + .expect("UTF-8"); + assert!(template.contains(PROTECTED_FIXTURE)); + assert!(template.contains("href=\"fetch-edited\"")); + assert!(template.contains(">fetch-edited

")); + assert!(!template.contains("serve-edited")); + assert!(!template.contains(""); + warm(&context, &target, 2).await; + { + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, 1); + assert_eq!(state.finalizations, 1, "the valid sibling stays reusable"); + let EnvelopeItem::Embedded { output_b64, .. } = &state.publications[0].item else { + panic!("the valid sibling should retain its embedded carrier"); + }; + assert_eq!( + STANDARD.decode(output_b64).expect("should decode output"), + state.cases[0].output + ); + } + drop(context); + let context = harness.context(&directory); + assert_eq!( + context + .store + .active_outcome_counts(now()) + .expect("should read durable operator outcomes") + .get("malformed_data_uri"), + Some(&1), + "unchanged refused media must not accumulate diagnostics for unrelated HTML changes" + ); +} + +#[tokio::test] +async fn reader_snapshot_instability_damps_only_the_changed_item_across_restart() { + use crate::store::Admission; + use trusted_server_provenance_encypher::preparation::protocol::ObservedItem; + + let harness = Harness::start(&["png", "jpeg", "webp"]).await; + let directory = tempfile::tempdir().expect("should create durable storage"); + let context = harness.context(&directory); + let target = harness.target(); + let observed: Vec<_> = { + let state = harness.boundary.lock().expect("should lock"); + state + .cases + .iter() + .map(|case| ObservedItem { + kind: ItemKind::Embedded, + identity: MediaIdentity { + credential_context: &context.credential_context, + policy_digest: &case.planned.request.expected_policy_digest, + page_url: &case.planned.request.page_url, + variant: &target.variant, + carrier: Carrier::Embedded, + mime: &case.planned.request.source.mime, + source_sha256: &case.planned.request.source.sha256, + } + .digest(), + }) + .collect() + }; + // The reader saw PNG + JPEG; the independently acquired snapshot now has WebP + JPEG. + harness.boundary.lock().expect("should lock").active = vec![2, 1]; + assert!(matches!( + context + .store + .admit(&target, JobOrigin::Signal, &observed[..2], now()) + .expect("should admit"), + Admission::Queued { .. } + )); + let claim = context + .store + .claim(now(), 1) + .expect("should claim") + .expect("should have work"); + run(&context, &claim) + .await + .expect("should settle the observed generation"); + assert!( + context + .store + .complete(&claim.job.key, claim.token, now()) + .expect("should acknowledge") + ); + let refusal = context + .store + .outcome(&target, &observed[0].identity) + .expect("should read outcome") + .expect("should record instability"); + assert_eq!(refusal.code, "source_unstable"); + assert_eq!(refusal.class, OutcomeClass::SourceUnstable); + assert!( + context + .store + .artifact(&observed[0].identity) + .expect("should read") + .is_none() + ); + assert!( + context + .store + .artifact(&observed[2].identity) + .expect("should read") + .is_none(), + "the unobserved replacement must not sign" + ); + { + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, 1); + assert_eq!(state.finalizations, 1); + assert_eq!(state.publications[0].identity, observed[1].identity); + } + drop(context); + let context = harness.context(&directory); + assert_eq!( + context + .store + .admit(&target, JobOrigin::Signal, &observed[..1], now()) + .expect("should admit"), + Admission::Damped, + "the same unstable observation remains damped after restart" + ); + assert!(matches!( + context + .store + .admit(&target, JobOrigin::Signal, &observed[1..], now()) + .expect("should admit new observation"), + Admission::Queued { .. } + )); + let claim = context + .store + .claim(now(), 2) + .expect("should claim") + .expect("should have changed work"); + run(&context, &claim) + .await + .expect("should prepare the newly observed replacement"); + assert!( + context + .store + .complete(&claim.job.key, claim.token, now()) + .expect("should acknowledge") + ); + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, 2); + assert_eq!( + state.finalizations, 2, + "the stable JPEG must not be re-signed" + ); + assert!( + state + .publications + .iter() + .all(|item| item.identity != observed[0].identity) + ); +} + +#[tokio::test] +async fn crash_after_finalize_recovers_pinned_geometry_and_probe_without_capabilities_or_new_sign() +{ + let harness = Harness::start(&["png"]).await; + let directory = tempfile::tempdir().expect("should create durable storage"); + { + let context = harness.context(&directory); + harness + .boundary + .lock() + .expect("should lock") + .lose_receipt_once = true; + let claim = claimed(&context, &harness.target(), 1); + assert_eq!( + run(&context, &claim) + .await + .expect_err("should lose receipt after committed finalize") + .class, + FailureClass::Transient + ); + assert_eq!(context.store.counts().expect("should count").artifacts, 0); + } + let previous_capabilities = { + let mut state = harness.boundary.lock().expect("should lock"); + state.closed = true; + assert_eq!(state.finalizations, 1); + state.capabilities + }; + let context = harness.context(&directory); + let page = harness.boundary.lock().expect("should lock").cases[0] + .planned + .request + .page_url + .clone(); + probe_ready(&context, &page) + .await + .expect("readiness must accept retained pinned releases while creation is closed"); + let claim = context + .store + .claim(now(), 2) + .expect("should recover durable claim") + .expect("should keep unfinished generation"); + assert!(matches!( + claim.job.state, + JobState::Processing { token: 2, .. } + )); + run(&context, &claim) + .await + .expect("should reconstruct exact completed receipt while creation is disabled"); + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, 1); + assert_eq!(state.finalizations, 1); + assert_eq!(state.finalize_attempts, 1); + assert_eq!( + state.capabilities, previous_capabilities, + "recovery bypasses creation capability admission" + ); + assert_eq!( + state.probe_fetches, 1, + "restart retains the exact authenticated release" + ); + assert_eq!(state.publications.len(), 1); +} + +#[tokio::test] +async fn real_external_media_uses_small_and_pinned_large_lanes_without_upload() { + for large in [false, true] { + let harness = Harness::start(&["external-png"]).await; + let directory = tempfile::tempdir().expect("should create durable storage"); + { + let mut state = harness.boundary.lock().expect("should lock"); + state.standalone = true; + state.large = large; + } + let context = harness.context(&directory); + warm(&context, &harness.target(), 1).await; + let published_identity = { + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.finalizations, 1); + assert_eq!(state.publications.len(), 1); + assert_eq!(state.range_reads > 0, large); + assert!(matches!(&state.publications[0].item, EnvelopeItem::Large { .. }) == large); + assert!(matches!(&state.publications[0].item, EnvelopeItem::Media { .. }) != large); + if let EnvelopeItem::Large { + planned, + etag, + origin_identity_sha256, + } = &state.publications[0].item + { + assert_eq!(origin_identity_sha256, &state.origin_identity_sha256); + assert_eq!( + state.publications[0].identity, + LargeLocator { + credential_context: &context.credential_context, + policy_digest: &planned.request.expected_policy_digest, + reader_url: &planned.request.page_url, + variant: "", + origin_host: &planned.request.source.origin_host, + origin_identity_sha256, + mime: &planned.request.source.mime, + etag, + length: planned.request.source.byte_length, + } + .digest() + ); + } + assert!( + !directory + .path() + .join("large") + .read_dir() + .is_ok_and(|mut entries| entries.next().is_some()), + "temporary pinned sources are removed before their reservations are released" + ); + state.publications[0].identity.clone() + }; + drop(context); + harness.boundary.lock().expect("should lock").closed = true; + let context = harness.context(&directory); + warm(&context, &harness.target(), 2).await; + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, 1); + assert_eq!(state.finalizations, 1); + assert_eq!(state.publications.len(), 2); + assert_eq!(state.publications[1].identity, published_identity); + } +} + +#[tokio::test] +async fn configured_origin_remap_refuses_large_acquisition_and_both_current_checks() { + for (remap_on_snapshot, preparations, finalizations) in [(2, 0, 0), (3, 1, 0), (4, 1, 1)] { + let harness = Harness::start(&["external-png"]).await; + let directory = tempfile::tempdir().expect("should create durable storage"); + { + let mut state = harness.boundary.lock().expect("should lock"); + state.standalone = true; + state.large = true; + state.remap_on_snapshot = Some(remap_on_snapshot); + } + let context = harness.context(&directory); + let target = harness.target(); + let claim = claimed(&context, &target, 1); + run(&context, &claim) + .await + .expect("should persist integrity refusal"); + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, preparations); + assert_eq!(state.finalizations, finalizations); + assert_eq!(state.range_reads, 1); + assert!(state.publications.is_empty()); + assert_eq!(context.store.counts().expect("should count").artifacts, 0); + let request = &state.cases[0].planned.request; + let expected_origin_identity = crate::hex_lower(&Sha256::digest( + b"https://origin.example.com/old/\0origin.example.com", + )); + let identity = LargeLocator { + credential_context: &context.credential_context, + policy_digest: &request.expected_policy_digest, + reader_url: &request.page_url, + variant: "", + origin_host: &request.source.origin_host, + origin_identity_sha256: &expected_origin_identity, + mime: &request.source.mime, + etag: &state.etag, + length: request.source.byte_length, + } + .digest(); + assert_eq!( + context + .store + .outcome(&target, &identity) + .expect("should read item outcome") + .expect("should retain configured-origin refusal") + .code, + "source_version_changed" + ); + } +} + +#[tokio::test] +async fn invalid_configured_origin_digest_refuses_large_work_before_acquisition() { + for invalid in ["".to_owned(), "not-a-digest".to_owned(), "A".repeat(64)] { + let harness = Harness::start(&["external-png"]).await; + let directory = tempfile::tempdir().expect("should create durable storage"); + { + let mut state = harness.boundary.lock().expect("should lock"); + state.standalone = true; + state.large = true; + state.origin_identity_sha256 = invalid.to_owned(); + } + let context = harness.context(&directory); + let target = harness.target(); + warm(&context, &target, 1).await; + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.range_reads, 0); + assert_eq!(state.preparations, 0); + assert_eq!(state.finalizations, 0); + assert!(state.publications.is_empty()); + assert_eq!( + context + .store + .outcome(&target, "*") + .expect("should read snapshot outcome") + .expect("should retain invalid-origin refusal") + .code, + "source_origin_identity_invalid" + ); + } +} + +#[tokio::test] +async fn truncated_pinned_source_is_refused_before_prepare_and_cleans_its_claim_owned_file() { + let harness = Harness::start(&["external-png"]).await; + let directory = tempfile::tempdir().expect("should create storage"); + { + let mut state = harness.boundary.lock().expect("should lock"); + state.standalone = true; + state.large = true; + state.short_range = true; + } + let context = harness.context(&directory); + warm(&context, &harness.target(), 1).await; + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, 0); + assert_eq!(state.finalizations, 0); + assert!(state.publications.is_empty()); + assert!( + context + .store + .reserved() + .expect("should retain only bounded refusal metadata") + < 4096 + ); + assert!( + !directory + .path() + .join("large") + .read_dir() + .expect("should have source directory") + .any(|entry| entry.is_ok()) + ); +} + +#[tokio::test] +async fn retired_completed_receipt_refuses_recovery_without_any_finalize_attempt() { + let harness = Harness::start(&["png"]).await; + let directory = tempfile::tempdir().expect("should create durable storage"); + { + let context = harness.context(&directory); + harness + .boundary + .lock() + .expect("should lock") + .lose_receipt_once = true; + let claim = claimed(&context, &harness.target(), 1); + run(&context, &claim) + .await + .expect_err("should simulate crash after API commit"); + } + { + let mut state = harness.boundary.lock().expect("should lock"); + state.closed = true; + state.retired_receipt = true; + } + let context = harness.context(&directory); + let claim = context + .store + .claim(now(), 2) + .expect("should reclaim") + .expect("should retain exact operation"); + run(&context, &claim) + .await + .expect("should record explicit terminal retirement refusal"); + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.finalizations, 1); + assert_eq!( + state.finalize_attempts, 1, + "HTTP410 must never fall through to a finalize call" + ); + assert_eq!(state.preparations, 1); + assert!(state.publications.is_empty()); + assert_eq!(context.store.counts().expect("should count").artifacts, 0); + let request = &state.cases[0].planned.request; + let identity = MediaIdentity { + credential_context: &context.credential_context, + policy_digest: &request.expected_policy_digest, + page_url: &request.page_url, + variant: "", + carrier: Carrier::Embedded, + mime: &request.source.mime, + source_sha256: &request.source.sha256, + } + .digest(); + assert_eq!( + context + .store + .outcome(&claim.job.target, &identity) + .expect("should retain outcome") + .expect("should expose retired refusal") + .code, + "result_retired" + ); +} + +#[tokio::test] +async fn corrupt_durable_output_is_refused_without_replanning_or_signing() { + let harness = Harness::start(&["png"]).await; + let directory = tempfile::tempdir().expect("should create storage"); + let context = harness.context(&directory); + warm(&context, &harness.target(), 1).await; + let identity = harness.boundary.lock().expect("should lock").publications[0] + .identity + .clone(); + let path = directory + .path() + .join("artifacts") + .join(format!("{identity}.json")); + let mut bytes = std::fs::read(&path).expect("should inspect durable output"); + bytes[0] ^= 1; + std::fs::write(path, bytes).expect("should simulate disk corruption"); + harness.boundary.lock().expect("should lock").closed = true; + let claim = claimed(&context, &harness.target(), 2); + assert_eq!( + run(&context, &claim) + .await + .expect_err("should refuse corrupt durable material") + .code, + "store_unavailable" + ); + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, 1); + assert_eq!(state.finalize_attempts, 1); + assert_eq!( + state.publications.len(), + 1, + "corrupt bytes never reach publication" + ); +} + +#[tokio::test] +async fn private_media_and_existing_real_embedded_provenance_are_never_replaced_or_uploaded() { + for existing_provenance in [false, true] { + let harness = Harness::start(&["png"]).await; + let directory = tempfile::tempdir().expect("should create storage"); + { + let mut state = harness.boundary.lock().expect("should lock"); + if existing_provenance { + state.cases[0].source = state.cases[0].output.clone(); + } else { + state.private = true; + } + } + let context = harness.context(&directory); + warm(&context, &harness.target(), 1).await; + let state = harness.boundary.lock().expect("should lock"); + assert_eq!(state.preparations, 0); + assert_eq!(state.finalize_attempts, 0); + assert!(state.publications.is_empty()); + assert_eq!(context.store.counts().expect("should count").artifacts, 0); + } +} diff --git a/crates/trusted-server-preparation-worker/src/prepare.rs b/crates/trusted-server-preparation-worker/src/prepare.rs new file mode 100644 index 0000000000..2c14dba028 --- /dev/null +++ b/crates/trusted-server-preparation-worker/src/prepare.rs @@ -0,0 +1,1138 @@ +//! One claimed job, end to end: a snapshot through the reader pipeline, +//! the same item extraction the reader runs, the per-item stability rule +//! of the job's origin, local prepared hashing, signing outside delivery, +//! and publication the delivery instance verifies through the API. +//! +//! A published item whose delivery record a reader still misses is +//! republished from its durable artifact, never signed again. A verified +//! plan is recorded before its finalize, so a retry after a crash +//! finalizes or recovers the same operation rather than planning a new +//! one. + +use std::io::Write as _; +use std::os::unix::fs::FileExt as _; +use std::path::PathBuf; +use std::sync::Arc; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use sha2::{Digest as _, Sha256}; + +use trusted_server_provenance_encypher::preparation::items::{ + DocumentKind, ItemContext, document_items, +}; +use trusted_server_provenance_encypher::preparation::pipeline::{ + self, Failure, FailureClass, MediaJob, PlannedMedia, SourceReader, +}; +use trusted_server_provenance_encypher::preparation::protocol::{ + Carrier, EnvelopeItem, ItemKind, LargeLocator, MediaIdentity, PREPARATION_PROTOCOL, + PUBLIC_SOURCE_PREFIX, ResultEnvelope, SnapshotMode, Target, credential_context, is_digest, + operation_key, +}; + +use crate::client::{Api, DiskProbeCache, Edge, Snapshot, WorkerConfig}; +use crate::store::{Artifact, Claim, JobOrigin, OutcomeClass, Store, outcome_class}; + +#[cfg(test)] +#[path = "media_tests.rs"] +mod media_tests; + +/// Everything a preparation task uses. +pub struct Context { + /// The durable store. + pub store: Arc, + /// The API. + pub api: Api, + /// The delivery instance. + pub edge: Edge, + /// The configuration. + pub config: Arc, + /// The credential context identities bind. + pub credential_context: String, +} + +impl Context { + /// The context for `config` with `api` and `edge`. + #[must_use] + pub fn new( + store: Arc, + api: Api, + edge: Edge, + config: Arc, + worker_token: &str, + ) -> Self { + let credential_context = + credential_context(&config.api_base_url, &config.domain, worker_token); + Self { + store, + api, + edge, + config, + credential_context, + } + } + + fn probes(&self) -> DiskProbeCache { + DiskProbeCache::new(Arc::clone(&self.store)) + } + + async fn load_probe( + &self, + capabilities: &pipeline::Capabilities, + ) -> Result { + let probe = pipeline::load_probe(&self.api, &self.probes(), capabilities).await?; + let release = probe.release(); + self.store + .refresh_context("probe", &format!("{}:{}", release.sha256, release.release)) + .map_err(|error| store_failure(&error))?; + Ok(probe) + } + + async fn load_release( + &self, + release: &pipeline::ProbeRelease, + ) -> Result { + pipeline::load_release(&self.api, &self.probes(), release).await + } +} + +pub(crate) async fn probe_ready(context: &Context, page: &str) -> Result<(), Failure> { + let _budget = TemporaryBudget::new(context, "temp:readiness".to_owned(), 8 * 1024 * 1024)?; + let releases = context + .store + .probe_releases() + .map_err(|error| store_failure(&error))?; + for release in &releases { + context.load_release(release).await?; + } + if !releases.is_empty() + || context + .store + .has_text_operations() + .map_err(|error| store_failure(&error))? + { + // Completed or interrupted work remains recoverable when creation is closed. + return Ok(()); + } + let capabilities = pipeline::capabilities(&context.api, page).await?; + context.load_probe(&capabilities).await?; + Ok(()) +} + +pub(crate) async fn refresh_probe_context(context: &Context, page: &str) -> Result<(), Failure> { + let _budget = TemporaryBudget::new(context, "temp:probe-observation".to_owned(), 64 * 1024)?; + let capabilities = pipeline::capabilities(&context.api, page).await?; + let (sha256, release) = pipeline::advertised_release_identity(&capabilities)?; + context + .store + .refresh_context("probe", &format!("{sha256}:{release}")) + .map_err(|error| store_failure(&error)) +} + +/// Unix seconds now. +#[must_use] +pub fn now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |elapsed| elapsed.as_secs()) +} + +/// One item of the current snapshot. +enum Work { + Text(pipeline::TextSignRequest), + Embedded { + mime: String, + source: Vec, + sha256: String, + }, + Media { + mime: String, + sha256: String, + }, + Large { + mime: String, + etag: String, + length: u64, + origin_identity_sha256: String, + }, +} + +/// The configured source and validators every acquisition read must retain. +#[derive(Clone, Copy)] +struct LargeVersion<'a> { + mime: &'a str, + etag: &'a str, + length: u64, + origin_identity_sha256: &'a str, +} + +struct Item { + identity: String, + kind: ItemKind, + work: Work, +} + +struct SnapshotItems { + items: Vec, + refusals: Vec<&'static str>, +} + +/// Runs one claimed job. An `Err` is transient and retried; every other +/// outcome is recorded per item. +/// +/// # Errors +/// +/// A transient [`Failure`]. +pub async fn run(context: &Context, claim: &Claim) -> Result<(), Failure> { + tokio::time::timeout( + std::time::Duration::from_secs(context.config.max_preparation_seconds), + run_claim(context, claim), + ) + .await + .map_err(|_| Failure::new(FailureClass::Transient, "preparation_deadline"))? +} + +async fn run_claim(context: &Context, claim: &Claim) -> Result<(), Failure> { + check_claim(context, claim)?; + // Unknown bounded source sizes reserve their configured ceiling before acquisition. + let _snapshot_budget = TemporaryBudget::new( + context, + format!("temp:snapshot:{}", claim.token), + (context.config.max_snapshot_bytes as u64).saturating_mul(4), + )?; + let job = &claim.job; + let target = &job.target; + let snapshot = match context + .edge + .snapshot_bounded( + target, + SnapshotMode::Whole, + context.config.max_snapshot_bytes, + ) + .await + { + Ok(snapshot) => snapshot, + Err(failure) + if failure.code == "snapshot_too_large" && context.config.max_large_media_bytes > 0 => + { + context.edge.snapshot(target, SnapshotMode::Head).await? + } + Err(failure) => return settle_failure(context, claim, &failure), + }; + if let Err(failure) = admit_snapshot(context, target, &snapshot) { + return settle_failure(context, claim, &failure); + } + let page_url = snapshot.meta.page_url.clone(); + let policy = pipeline::policy_digest(&context.api, &page_url).await?; + let SnapshotItems { items, refusals } = match current_items(context, target, &snapshot, &policy) + { + Ok(items) => items, + Err(failure) => return settle_failure(context, claim, &failure), + }; + let identities: Vec = items.iter().map(|item| item.identity.clone()).collect(); + let previous = context + .store + .observe_claim_snapshot(claim, &identities, now()) + .map_err(|error| store_failure(&error))? + .unwrap_or_default(); + let mut transient = + record_extraction_refusals(context, claim, &snapshot, &policy, refusals).err(); + let selected: Vec<&Item> = match job.origin { + JobOrigin::Signal => { + for observed in &job.observed { + if !identities.contains(&observed.identity) { + // The snapshot is not what the reader saw: damped per + // item, never signed as if it were. + if let Err(failure) = store_outcome( + context, + claim, + &observed.identity, + "source_unstable", + OutcomeClass::SourceUnstable, + ) { + transient.get_or_insert(failure); + } + } + } + items + .iter() + .filter(|item| { + job.observed + .iter() + .any(|observed| observed.identity == item.identity) + }) + .collect() + } + JobOrigin::Reconcile => items + .iter() + .filter(|item| previous.contains(&item.identity)) + .collect(), + JobOrigin::Warm => items.iter().collect(), + }; + for item in selected { + if context + .store + .outcome(target, &item.identity) + .map_err(|error| store_failure(&error))? + .is_some_and(|outcome| now() < outcome.until) + { + continue; + } + match prepare_item(context, target, &snapshot, &policy, item, claim).await { + Ok(()) => {} + Err(failure) + if matches!( + failure.class, + FailureClass::Transient | FailureClass::PolicyChanged | FailureClass::Missing + ) => + { + transient.get_or_insert(failure); + } + Err(failure) => { + log::info!( + "preparation: item refused kind={:?} code={}", + item.kind, + failure.code + ); + if let Some(class) = outcome_class(failure.class) + && let Err(failure) = + store_outcome(context, claim, &item.identity, &failure.code, class) + { + transient.get_or_insert(failure); + } + } + } + } + transient.map_or(Ok(()), Err) +} + +fn store_failure(error: &crate::store::StoreError) -> Failure { + match error { + crate::store::StoreError::Quota => Failure::new(FailureClass::Transient, "quota_exhausted"), + _ => Failure::new(FailureClass::Transient, "store_unavailable"), + } +} + +fn store_outcome( + context: &Context, + claim: &Claim, + identity: &str, + code: &str, + class: OutcomeClass, +) -> Result<(), Failure> { + context + .store + .record_claim_outcome(claim, identity, code, class, now()) + .map_err(|error| store_failure(&error)) +} + +/// Retain extraction refusals without damping the whole target or its valid +/// siblings. One outcome per reason and admitted page scope is a diagnostic, +/// not a count of assets or unrelated document revisions. +fn record_extraction_refusals( + context: &Context, + claim: &Claim, + snapshot: &Snapshot, + policy: &str, + mut refusals: Vec<&'static str>, +) -> Result<(), Failure> { + // These nested bytes are already covered by the containing SVG item. + refusals.retain(|reason| *reason != "inside_inline_svg"); + if refusals.is_empty() { + return Ok(()); + } + refusals.sort_unstable(); + refusals.dedup(); + let mut scope = Sha256::new(); + scope.update(b"encypher.document-extraction-refusal.v2\0"); + for part in [ + context.credential_context.as_bytes(), + policy.as_bytes(), + snapshot.meta.page_url.as_bytes(), + claim.job.target.variant.as_bytes(), + ] { + scope.update((part.len() as u64).to_be_bytes()); + scope.update(part); + } + for reason in refusals { + let mut digest = scope.clone(); + digest.update(reason.as_bytes()); + let identity = crate::hex_lower(&digest.finalize()); + store_outcome(context, claim, &identity, reason, OutcomeClass::Terminal)?; + } + Ok(()) +} + +fn settle_failure(context: &Context, claim: &Claim, failure: &Failure) -> Result<(), Failure> { + let Some(class) = outcome_class(failure.class) else { + return Err(failure.clone()); + }; + if claim.job.observed.is_empty() { + store_outcome(context, claim, "*", &failure.code, class)?; + } + for item in &claim.job.observed { + store_outcome(context, claim, &item.identity, &failure.code, class)?; + } + Ok(()) +} + +/// The items of the snapshot, by the reader's own extraction. +fn current_items( + context: &Context, + target: &Target, + snapshot: &Snapshot, + policy: &str, +) -> Result { + let meta = &snapshot.meta; + let media_type = meta + .content_type + .split(';') + .next() + .unwrap_or_default() + .trim() + .to_ascii_lowercase(); + let item_context = ItemContext { + credential_context: &context.credential_context, + policy_digest: policy, + page_url: &meta.page_url, + variant: &target.variant, + article_selector: &context.config.article_selector, + min_text_length: context.config.min_text_length, + max_media_bytes: context.config.max_media_bytes, + }; + let document_kind = match media_type.as_str() { + "text/html" => Some(DocumentKind::Page), + "text/css" => Some(DocumentKind::Stylesheet), + _ => None, + }; + if let Some(kind) = document_kind { + let document = std::str::from_utf8(&snapshot.bytes) + .map_err(|_| Failure::new(FailureClass::Unsupported, "not_utf8"))?; + let found = document_items(&item_context, document, kind, None); + let mut items = Vec::new(); + if let Some(text) = found.text { + items.push(Item { + identity: text.identity, + kind: ItemKind::Text, + work: Work::Text(text.request), + }); + } + for embedded in found.embedded { + items.push(Item { + identity: embedded.identity, + kind: ItemKind::Embedded, + work: Work::Embedded { + mime: embedded.mime, + source: embedded.source, + sha256: embedded.sha256, + }, + }); + } + let mut refusals = found.refused; + refusals.extend(found.text_refusal); + return Ok(SnapshotItems { items, refusals }); + } + if snapshot.bytes.is_empty() { + if !is_digest(&meta.origin.origin_identity_sha256) { + return Err(Failure::new( + FailureClass::Integrity, + "source_origin_identity_invalid", + )); + } + let (Some(etag), Some(length)) = (meta.etag.clone(), meta.length) else { + return Err(Failure::new(FailureClass::Unsupported, "no_strong_etag")); + }; + if length > context.config.max_large_media_bytes { + return Err(Failure::new(FailureClass::Unsupported, "media_too_large")); + } + let identity = LargeLocator { + credential_context: &context.credential_context, + policy_digest: policy, + reader_url: &meta.page_url, + variant: &target.variant, + origin_host: &meta.origin.origin_host, + origin_identity_sha256: &meta.origin.origin_identity_sha256, + mime: &media_type, + etag: &etag, + length, + } + .digest(); + return Ok(SnapshotItems { + items: vec![Item { + identity, + kind: ItemKind::Large, + work: Work::Large { + mime: media_type, + etag, + length, + origin_identity_sha256: meta.origin.origin_identity_sha256.clone(), + }, + }], + refusals: Vec::new(), + }); + } + let sha256 = crate::hex_lower(&Sha256::digest(&snapshot.bytes)); + let identity = MediaIdentity { + credential_context: &context.credential_context, + policy_digest: policy, + page_url: &meta.page_url, + variant: &target.variant, + carrier: Carrier::External, + mime: &media_type, + source_sha256: &sha256, + } + .digest(); + Ok(SnapshotItems { + items: vec![Item { + identity, + kind: ItemKind::Media, + work: Work::Media { + mime: media_type, + sha256, + }, + }], + refusals: Vec::new(), + }) +} + +fn admit_snapshot(context: &Context, target: &Target, snapshot: &Snapshot) -> Result<(), Failure> { + let meta = &snapshot.meta; + if meta.protocol != PREPARATION_PROTOCOL + || meta.page_url != format!("https://{}{}", context.config.public_host, target.path) + { + return Err(Failure::new( + FailureClass::Integrity, + "snapshot_identity_mismatch", + )); + } + if meta.origin.private { + return Err(Failure::new(FailureClass::Privacy, "source_private")); + } + if meta.origin.no_transform || meta.origin.manifest_link { + return Err(Failure::new( + FailureClass::ExistingProvenance, + "source_protected", + )); + } + if !matches!(meta.origin.status, 200 | 206) + || (!meta.origin.content_encoding.is_empty() + && !meta + .origin + .content_encoding + .eq_ignore_ascii_case("identity")) + { + return Err(Failure::new( + FailureClass::Unsupported, + "source_not_admitted", + )); + } + Ok(()) +} + +struct TemporaryBudget { + store: Arc, + owner: String, +} + +impl TemporaryBudget { + fn new(context: &Context, owner: String, bytes: u64) -> Result { + reserve(context, &owner, bytes)?; + Ok(Self { + store: Arc::clone(&context.store), + owner, + }) + } +} + +impl Drop for TemporaryBudget { + fn drop(&mut self) { + if self.store.release(&self.owner).is_err() { + log::warn!("preparation: temporary reservation retained for restart cleanup"); + } + } +} + +struct ArtifactBudget<'a> { + context: &'a Context, + claim: &'a Claim, + identity: &'a str, +} + +impl Drop for ArtifactBudget<'_> { + fn drop(&mut self) { + // A newer claim may already be acquiring this identity. A stale + // attempt never releases shared source/output ownership underneath it. + if check_claim(self.context, self.claim).is_err() { + return; + } + match self.context.store.has_reconstruction(self.identity) { + Ok(false) => { + if self + .context + .store + .release(&format!("artifact:{}", self.identity)) + .is_err() + || self + .context + .store + .discard_unplanned_sources(self.identity) + .is_err() + { + log::warn!( + "preparation: unplanned material remains charged for restart cleanup" + ); + } + } + Ok(true) => {} + Err(_) => log::warn!("preparation: uncertain output reservation remains charged"), + } + } +} + +fn check_claim(context: &Context, claim: &Claim) -> Result<(), Failure> { + let current = context + .store + .job(&claim.job.key) + .map_err(|error| store_failure(&error))?; + if current.is_some_and(|job| matches!(job.state, crate::store::JobState::Processing { token, lease_until } if token == claim.token && lease_until > now())) { + Ok(()) + } else { + Err(Failure::new(FailureClass::Transient, "lease_lost")) + } +} + +/// Prepares and publishes one item, or republishes its artifact. +async fn prepare_item( + context: &Context, + target: &Target, + snapshot: &Snapshot, + policy: &str, + item: &Item, + claim: &Claim, +) -> Result<(), Failure> { + check_claim(context, claim)?; + if context + .store + .retirement(&item.identity) + .map_err(|error| store_failure(&error))? + .is_some() + { + return Ok(()); + } + if let Some(artifact) = context + .store + .artifact(&item.identity) + .map_err(|error| store_failure(&error))? + { + // Republished from the durable artifact: the delivery instance + // recovers the same receipt, and nothing is signed again. + context.edge.publish(&artifact.envelope).await?; + context + .store + .acknowledge_artifact(&item.identity) + .map_err(|error| store_failure(&error))?; + return Ok(()); + } + let epoch = context + .store + .retirement_epoch() + .map_err(|error| store_failure(&error))?; + let meta = &snapshot.meta; + let page_url = meta.page_url.as_str(); + let source_size = match &item.work { + Work::Text(request) => request.text.len() as u64, + Work::Embedded { source, .. } => source.len() as u64, + Work::Media { .. } => snapshot.bytes.len() as u64, + Work::Large { .. } => 0, + }; + // Source copies, base64 encodings, probe transfer, manifest, carrier and + // output assembly are all bounded and charged before any API creation. + let _assembly_budget = TemporaryBudget::new( + context, + format!("temp:assembly:{}", claim.token), + source_size + .saturating_mul(8) + .saturating_add(16 * 1024 * 1024), + )?; + reserve( + context, + &format!("artifact:{}", item.identity), + source_size + .saturating_mul(4) + .saturating_add(8 * 1024 * 1024), + )?; + let _artifact_budget = ArtifactBudget { + context, + claim, + identity: &item.identity, + }; + let envelope_item = match &item.work { + Work::Text(request) => { + let key = operation_key(&item.identity, 0); + match pipeline::recover_text(&context.api, request, &key).await { + Ok(_) => { + context + .store + .begin_text(&item.identity, request) + .map_err(|error| store_failure(&error))?; + } + Err(failure) if failure.class == FailureClass::Missing => { + if !context + .store + .begin_text(&item.identity, request) + .map_err(|error| store_failure(&error))? + { + return Err(Failure::new( + FailureClass::Integrity, + "text_result_unrecoverable", + )); + } + check_claim(context, claim)?; + pipeline::sign_text(&context.api, request, &key).await?; + } + Err(failure) => return Err(failure), + } + EnvelopeItem::Text { + request: request.clone(), + idempotency_key: key, + } + } + Work::Embedded { + mime, + source, + sha256, + } => { + keep_public_source(context, sha256, mime, source, &item.identity)?; + let reference = format!( + "https://{}{PUBLIC_SOURCE_PREFIX}{sha256}", + context.config.public_host + ); + let job = MediaJob { + page_url, + policy_digest: policy, + carrier: Carrier::Embedded, + mime, + source_sha256: sha256, + reference: &reference, + origin_host: &meta.origin.origin_host, + reference_kind: "publisher_snapshot", + idempotency_key: &operation_key(&item.identity, 0), + }; + let planned = planned(context, &item.identity, &job, source.as_slice()).await?; + let output = recover_or_finalize( + context, + claim, + &item.identity, + &planned, + Some(source.as_slice()), + ) + .await? + .ok_or_else(|| Failure::new(FailureClass::Integrity, "no_output"))?; + EnvelopeItem::Embedded { + planned, + source_b64: STANDARD.encode(source), + output_b64: STANDARD.encode(output), + } + } + Work::Media { mime, sha256 } => { + let source = snapshot.bytes.as_ref(); + let origin = &meta.origin; + let origin_reference = origin.source_reference.as_deref().filter(|reference| { + url::Url::parse(reference).is_ok_and(|url| { + url.scheme() == "https" && url.host_str() == Some(origin.origin_host.as_str()) + }) + }); + let snapshot_reference = format!( + "https://{}{PUBLIC_SOURCE_PREFIX}{sha256}", + context.config.public_host + ); + let (reference, kind, origin_host) = match origin_reference { + Some(reference) => (reference.to_owned(), "origin", origin.origin_host.clone()), + None => { + keep_public_source(context, sha256, mime, source, &item.identity)?; + ( + snapshot_reference, + "publisher_snapshot", + origin.origin_host.clone(), + ) + } + }; + let job = MediaJob { + page_url, + policy_digest: policy, + carrier: Carrier::External, + mime, + source_sha256: sha256, + reference: &reference, + origin_host: &origin_host, + reference_kind: kind, + idempotency_key: &operation_key(&item.identity, 0), + }; + let planned = planned(context, &item.identity, &job, source).await?; + recover_or_finalize(context, claim, &item.identity, &planned, None).await?; + EnvelopeItem::Media { + planned, + source_b64: STANDARD.encode(source), + } + } + Work::Large { + mime, + etag, + length, + origin_identity_sha256, + } => { + let version = LargeVersion { + mime, + etag, + length: *length, + origin_identity_sha256, + }; + let file = + download_large(context, target, &item.identity, claim.token, version).await?; + let reference = meta.origin.source_reference.clone().ok_or_else(|| { + Failure::new(FailureClass::Unsupported, "source_reference_required") + })?; + let job = MediaJob { + page_url, + policy_digest: policy, + carrier: Carrier::External, + mime, + source_sha256: &file.sha256, + reference: &reference, + origin_host: &meta.origin.origin_host, + reference_kind: "origin", + idempotency_key: &operation_key(&item.identity, 0), + }; + let result = async { + let planned = planned(context, &item.identity, &job, &file).await?; + confirm_large(context, target, version).await?; + recover_or_finalize(context, claim, &item.identity, &planned, None).await?; + confirm_large(context, target, version).await?; + Ok::<_, Failure>(planned) + } + .await; + EnvelopeItem::Large { + planned: result?, + etag: etag.clone(), + origin_identity_sha256: origin_identity_sha256.clone(), + } + } + }; + let envelope = ResultEnvelope { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: context.config.deployment_id.clone(), + identity: item.identity.clone(), + retirement_epoch: epoch, + variant: target.variant.clone(), + item: envelope_item, + }; + let artifact = Artifact { + envelope, + target: target.clone(), + acknowledged: false, + }; + // The result is durable before it is acknowledged, and never outlives a + // retirement that happened while it was prepared. + if !context + .store + .record_artifact(&artifact) + .map_err(|error| store_failure(&error))? + { + context + .store + .release(&format!("artifact:{}", item.identity)) + .map_err(|error| store_failure(&error))?; + return Ok(()); + } + check_claim(context, claim)?; + if context + .store + .retirement(&item.identity) + .map_err(|error| store_failure(&error))? + .is_some() + { + return Ok(()); + } + context.edge.publish(&artifact.envelope).await?; + context + .store + .acknowledge_artifact(&item.identity) + .map_err(|error| store_failure(&error)) +} + +async fn recover_or_finalize( + context: &Context, + claim: &Claim, + identity: &str, + planned: &PlannedMedia, + source: Option<&[u8]>, +) -> Result>, Failure> { + check_claim(context, claim)?; + if context + .store + .retirement(identity) + .map_err(|error| store_failure(&error))? + .is_some() + { + return Err(Failure::new(FailureClass::Integrity, "retired")); + } + let probe = context.load_release(&planned.release).await?; + match pipeline::recover_media(&context.api, &planned.request, &planned.idempotency_key).await { + Ok(recovered) => pipeline::reconstruct_media(&probe, planned, &recovered, source).await, + Err(failure) if failure.class == FailureClass::Missing => { + check_claim(context, claim)?; + if context + .store + .retirement(identity) + .map_err(|error| store_failure(&error))? + .is_some() + { + return Err(Failure::new(FailureClass::Integrity, "retired")); + } + pipeline::finalize_media(&context.api, &probe, planned, source).await + } + Err(failure) => Err(failure), + } +} + +/// The verified plan of an identity: the one recorded before an earlier +/// finalize, or a new one, recorded before it is finalized. +async fn planned( + context: &Context, + identity: &str, + job: &MediaJob<'_>, + source: &S, +) -> Result { + if let Some(planned) = context + .store + .plan(identity) + .map_err(|error| store_failure(&error))? + { + if planned.request.source.sha256 != job.source_sha256 + || planned.request.source.byte_length != source.size() + || planned.request.source.mime != job.mime + || planned.request.expected_policy_digest != job.policy_digest + { + return Err(Failure::new( + FailureClass::Integrity, + "stored_plan_source_mismatch", + )); + } + return Ok(planned); + } + let capabilities = pipeline::capabilities(&context.api, job.page_url).await?; + let probe = context.load_probe(&capabilities).await?; + let planned = pipeline::plan_media(&context.api, &probe, &capabilities, job, source).await?; + context + .store + .record_plan(identity, &planned) + .map_err(|error| store_failure(&error))?; + Ok(planned) +} + +/// Keeps an admitted public source snapshot the publisher route serves by +/// digest, within the byte quota. +fn keep_public_source( + context: &Context, + sha256: &str, + mime: &str, + bytes: &[u8], + identity: &str, +) -> Result<(), Failure> { + context + .store + .keep_source(sha256, mime, bytes, identity) + .map_err(|error| store_failure(&error)) +} + +/// Reserves `bytes` for `owner` against the one hard quota every task +/// shares, in a store transaction, before anything is written. A full +/// quota refuses the work and retries later; it never evicts a retained +/// result. +fn reserve(context: &Context, owner: &str, bytes: u64) -> Result<(), Failure> { + if context + .store + .reserve(owner, bytes) + .map_err(|error| store_failure(&error))? + { + Ok(()) + } else { + Err(Failure::new(FailureClass::Transient, "quota_exhausted")) + } +} + +/// A large source streamed to a private file through pinned range reads. +struct LargeFile { + file: std::fs::File, + size: u64, + sha256: String, + _cleanup: TemporarySource, +} + +struct TemporarySource { + store: Arc, + owner: String, + path: PathBuf, +} + +impl Drop for TemporarySource { + fn drop(&mut self) { + let removed = match std::fs::remove_file(&self.path) { + Ok(()) => true, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => true, + Err(_) => false, + }; + if removed { + let synced = self.path.parent().is_some_and(|parent| { + std::fs::File::open(parent) + .and_then(|file| file.sync_all()) + .is_ok() + }); + if synced && self.store.release(&self.owner).is_ok() { + return; + } + } + log::warn!("preparation: temporary source remains charged for restart cleanup"); + } +} + +impl SourceReader for LargeFile { + fn size(&self) -> u64 { + self.size + } + + async fn read_exact_at(&self, offset: u64, destination: &mut [u8]) -> Result<(), Failure> { + self.file + .read_exact_at(destination, offset) + .map_err(|_| Failure::new(FailureClass::Transient, "source_read")) + } +} + +async fn confirm_large( + context: &Context, + target: &Target, + version: LargeVersion<'_>, +) -> Result<(), Failure> { + let LargeVersion { + mime, + etag, + length, + origin_identity_sha256, + } = version; + let current = context.edge.snapshot(target, SnapshotMode::Head).await?; + admit_snapshot(context, target, ¤t)?; + if current.meta.etag.as_deref() != Some(etag) + || current.meta.length != Some(length) + || current.meta.origin.origin_identity_sha256 != origin_identity_sha256 + || current + .meta + .content_type + .split(';') + .next() + .unwrap_or_default() + .trim() + != mime + { + return Err(Failure::new( + FailureClass::Integrity, + "source_version_changed", + )); + } + Ok(()) +} + +/// Streams every byte of the version `etag` names, in pinned ranges read +/// through the reader pipeline, into a private file, hashing as it goes. +async fn download_large( + context: &Context, + target: &Target, + identity: &str, + token: u64, + version: LargeVersion<'_>, +) -> Result { + let LargeVersion { + mime, + etag, + length, + origin_identity_sha256, + } = version; + const CHUNK: u64 = 8 * 1024 * 1024; + if length == 0 + || length > context.config.max_large_media_bytes + || !is_digest(origin_identity_sha256) + || !etag.starts_with('"') + || !etag.ends_with('"') + || etag.len() < 2 + || etag.bytes().any(|byte| byte.is_ascii_control()) + { + return Err(Failure::new( + FailureClass::Unsupported, + "no_strong_source_version", + )); + } + let name = format!("{identity}-{token}"); + let owner = format!("temp:large:{name}"); + reserve(context, &owner, length)?; + let dir = context.config.store_dir.join("large"); + std::fs::create_dir_all(&dir).map_err(|_| Failure::new(FailureClass::Transient, "storage"))?; + let path = dir.join(name); + let cleanup = TemporarySource { + store: Arc::clone(&context.store), + owner, + path: path.clone(), + }; + let mut file = std::fs::OpenOptions::new() + .write(true) + .read(true) + .create_new(true) + .open(&path) + .map_err(|_| Failure::new(FailureClass::Transient, "storage"))?; + let mut hasher = Sha256::new(); + let mut first = 0_u64; + while first < length { + let last = (first + CHUNK).min(length) - 1; + let part = context + .edge + .snapshot( + target, + SnapshotMode::Range { + first, + last, + if_match: etag.to_owned(), + }, + ) + .await; + let part = part?; + admit_snapshot(context, target, &part)?; + if part.meta.etag.as_deref() != Some(etag) + || part.meta.length != Some(length) + || part.meta.origin.origin_identity_sha256 != origin_identity_sha256 + || part.bytes.len() as u64 != last - first + 1 + || part + .meta + .content_type + .split(';') + .next() + .unwrap_or_default() + .trim() + != mime + { + return Err(Failure::new( + FailureClass::Integrity, + "source_version_changed", + )); + } + hasher.update(&part.bytes); + file.write_all(&part.bytes) + .map_err(|_| Failure::new(FailureClass::Transient, "storage"))?; + first = last + 1; + } + file.sync_all() + .map_err(|_| Failure::new(FailureClass::Transient, "storage"))?; + Ok(LargeFile { + _cleanup: cleanup, + file, + size: length, + sha256: crate::hex_lower(&hasher.finalize()), + }) +} diff --git a/crates/trusted-server-preparation-worker/src/store.rs b/crates/trusted-server-preparation-worker/src/store.rs new file mode 100644 index 0000000000..6af42c92e9 --- /dev/null +++ b/crates/trusted-server-preparation-worker/src/store.rs @@ -0,0 +1,2463 @@ +//! The worker's durable state: one exclusively owned redb database whose +//! transactions serialize admission, fixed-capacity slots, claim fencing, +//! attempts, damping, inventory, retirement and the marker outbox. +//! +//! Every write is one redb write transaction, which commits atomically or +//! not at all, and redb admits one writer at a time, so no lock or +//! compare-and-swap is fabricated over anything else. A second process +//! cannot open the same file: redb holds an exclusive lock on it, and the +//! worker refuses to start without it. Management and the CLI go through +//! the owner's HTTP routes, never through a second open. + +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +use redb::{ + Database, ReadableDatabase as _, ReadableTable as _, ReadableTableMetadata as _, + TableDefinition, +}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +use trusted_server_provenance_encypher::preparation::pipeline::{FailureClass, PlannedMedia}; +use trusted_server_provenance_encypher::preparation::protocol::{ + MarkerEnvelope, ObservedItem, PREPARATION_PROTOCOL, ResultEnvelope, Target, is_digest, +}; + +#[path = "store_files.rs"] +mod files; +#[path = "store_maintenance.rs"] +mod maintenance; + +const JOBS: TableDefinition<&str, &[u8]> = TableDefinition::new("jobs"); +const OUTCOMES: TableDefinition<&str, &[u8]> = TableDefinition::new("outcomes"); +const ARTIFACTS: TableDefinition<&str, &[u8]> = TableDefinition::new("artifacts"); +const PLANS: TableDefinition<&str, &[u8]> = TableDefinition::new("plans"); +const INVENTORY: TableDefinition<&str, &[u8]> = TableDefinition::new("inventory"); +const RETIRED: TableDefinition<&str, &[u8]> = TableDefinition::new("retired"); +/// Hard byte reservations by owner: `source:`, `probe:`, +/// `artifact:` for retained material, `temp:` for a +/// large source being read. Reserved before anything is written. +const QUOTA: TableDefinition<&str, u64> = TableDefinition::new("quota"); +const OUTBOX: TableDefinition<&str, &[u8]> = TableDefinition::new("outbox"); +const SOURCES: TableDefinition<&str, &[u8]> = TableDefinition::new("sources"); +const META: TableDefinition<&str, &[u8]> = TableDefinition::new("meta"); +const TEXT_OPERATIONS: TableDefinition<&str, &[u8]> = TableDefinition::new("text_operations"); + +fn reserve_in( + tx: &redb::WriteTransaction, + owner: &str, + bytes: u64, + limit: u64, +) -> Result { + let mut quota = tx.open_table(QUOTA).map_err(storage)?; + let mut used = 0_u64; + let mut held = 0_u64; + for entry in quota.iter().map_err(storage)? { + let (key, value) = entry.map_err(storage)?; + used = used + .checked_add(value.value()) + .ok_or_else(|| storage("quota overflow"))?; + if key.value() == owner { + held = value.value(); + } + } + let wanted = held.max(bytes); + if used + .saturating_sub(held) + .checked_add(wanted) + .is_none_or(|total| total > limit) + { + return Ok(false); + } + quota.insert(owner, wanted).map_err(storage)?; + Ok(true) +} +const PROBE_REFERENCES: TableDefinition<&str, &[u8]> = TableDefinition::new("probe_references"); + +fn put_outcome( + tx: &redb::WriteTransaction, + target: &Target, + identity: &str, + outcome: &Outcome, + limit: u64, +) -> Result<(), StoreError> { + let key = outcome_key(target, identity); + let encoded = encode(outcome)?; + if !reserve_in( + tx, + &format!("outcome:{key}"), + (key.len() + encoded.len()) as u64, + limit, + )? { + return Err(StoreError::Quota); + } + tx.open_table(OUTCOMES) + .map_err(storage)? + .insert(key.as_str(), encoded.as_slice()) + .map_err(storage)?; + Ok(()) +} + +fn damp_exhausted( + tx: &redb::WriteTransaction, + job: &Job, + code: &str, + now: u64, + limit: u64, +) -> Result<(), StoreError> { + let outcome = Outcome { + code: code.to_owned(), + class: OutcomeClass::Exhausted, + until: now.saturating_add(3600), + }; + for item in &job.observed { + put_outcome(tx, &job.target, &item.identity, &outcome, limit)?; + } + put_outcome(tx, &job.target, "*", &outcome, limit) +} + +/// Why a store operation failed. +#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] +pub enum StoreError { + /// Another process owns the database. + #[display("the preparation store is owned by another process")] + AlreadyOwned, + /// The database failed. + #[display("the preparation store failed: {_0}")] + Storage(String), + /// A hard byte reservation could not be obtained. + #[display("the preparation artifact quota is exhausted")] + Quota, + /// Independent inventory capacity has been reached. + #[display("the preparation inventory is full")] + InventoryFull, +} + +impl core::error::Error for StoreError {} + +fn storage(error: E) -> StoreError { + StoreError::Storage(error.to_string()) +} + +/// The fixed bounds the store enforces in its transactions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Bounds { + /// Most jobs, of any origin. + pub max_jobs: usize, + /// Most queued job metadata bytes. + pub max_queue_bytes: usize, + /// Slots reader hints can never take. + pub reserved_jobs: usize, + /// Bytes reader hints can never take. + pub reserved_bytes: usize, + /// Most attempts of one job. + pub max_attempts: u32, + /// Longest one job is retried, in seconds. + pub retry_window_seconds: u64, + /// Longest backoff, in seconds. + pub max_backoff_seconds: u64, + /// Claim lease, in seconds. + pub lease_seconds: u64, + /// Most inventory targets. + pub max_inventory: usize, + /// Most items one job holds. + pub max_items_per_job: usize, + /// The hard byte quota of sources, temporary files, probes and retained + /// outputs together. + pub quota_bytes: u64, +} + +impl Default for Bounds { + fn default() -> Self { + Self { + max_jobs: 1024, + max_queue_bytes: 8 * 1024 * 1024, + reserved_jobs: 256, + reserved_bytes: 2 * 1024 * 1024, + max_attempts: 6, + retry_window_seconds: 24 * 3600, + max_backoff_seconds: 300, + lease_seconds: 120, + max_inventory: 1024, + max_items_per_job: 64, + quota_bytes: 10 * 1024 * 1024 * 1024, + } + } +} + +/// What started a job, which decides its capacity and stability rule. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum JobOrigin { + /// A reader's hint: only observed items, only while they match. + Signal, + /// An authenticated publication event or operator warm: the current + /// snapshot is intentionally selected. + Warm, + /// Periodic reconciliation: only items stable across two snapshots. + Reconcile, +} + +/// A job's state. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "state", rename_all = "snake_case")] +pub enum JobState { + /// Waiting until `ready_at`. + Pending { + /// Unix seconds it may be claimed from. + ready_at: u64, + }, + /// Claimed under `token` until `lease_until`. + Processing { + /// The claim fence. + token: u64, + /// Unix seconds the lease ends. + lease_until: u64, + }, +} + +/// One durable job. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Job { + /// Its key, the target's. + pub key: String, + /// The target. + pub target: Target, + /// What started it. + pub origin: JobOrigin, + /// Items a reader observed, for a signal job. + pub observed: Vec, + /// Bumped whenever a newer observation replaces the desired state. + pub generation: u64, + /// A newer observation arrived while it was processing. + pub follow_up: bool, + /// Attempts made. + pub attempts: u32, + /// Unix seconds it was first admitted. + pub admitted_at: u64, + /// Its state. + pub state: JobState, + /// Whether it holds a reserved slot. + pub reserved: bool, +} + +/// A claimed job, fenced by its token. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Claim { + /// The job as claimed. + pub job: Job, + /// The fence. + pub token: u64, +} + +/// The damped outcome of one item. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Outcome { + /// Its code. + pub code: String, + /// Its class. + pub class: OutcomeClass, + /// Unix seconds it stops suppressing hints, `u64::MAX` until the + /// identity changes or an operator retries. + pub until: u64, +} + +/// How an item's outcome damps further work on it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OutcomeClass { + /// Its snapshot did not match the observation: five minutes. + SourceUnstable, + /// Retries exhausted: one hour. + Exhausted, + /// Integrity, privacy, unsupported carrier or existing provenance: + /// until the identity changes or an operator retries. + Terminal, +} + +/// What admission decided. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "outcome", rename_all = "snake_case")] +pub enum Admission { + /// Committed as a new job. + Queued { + /// The job key. + key: String, + }, + /// Merged into an existing job for the same target. + Coalesced { + /// The job key. + key: String, + }, + /// Every item is damped, retired or already published. + Damped, + /// No capacity; a later submission is safe. + QueueFull, +} + +/// A published identity's durable artifact: the envelope the delivery +/// instance verified, kept to republish after cache eviction without a +/// new sign operation. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Artifact { + /// The envelope. + pub envelope: ResultEnvelope, + /// The target it belongs to. + pub target: Target, + /// Whether the delivery instance acknowledged it. + pub acknowledged: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ArtifactFile { + sha256: String, + bytes: u64, + acknowledged: bool, +} + +/// A retired identity. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Retirement { + /// Its epoch. + pub epoch: u64, + /// Whether the delivery instance acknowledged its marker. This is not + /// proof every replica or downstream cache changed. + pub acknowledged: bool, +} + +/// One inventory target and the identities of its last snapshot. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InventoryEntry { + /// The target. + pub target: Target, + /// Unix seconds it was last reconciled. + pub last_scan: u64, + /// The item identities its last snapshot held. + pub last_identities: Vec, +} + +/// A source snapshot held on the worker. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SourceRecord { + /// Its media type. + pub mime: String, + /// Whether the page and asset passed public admission, so the publisher + /// route may serve it. + pub public: bool, + /// Identities whose retained artifacts reference it. + pub references: Vec, +} + +/// The store's counts, for status. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Counts { + /// Pending jobs. + pub pending: usize, + /// Processing jobs. + pub processing: usize, + /// Retained artifacts, independently of publication acknowledgement. + pub artifacts: usize, + /// Retained artifacts awaiting an acknowledged result PUT. + pub publication_pending: usize, + /// Result PUTs acknowledged by the delivery instance. + pub publication_acknowledged: usize, + /// Retirements awaiting marker acknowledgement. + pub retirement_pending: usize, + /// Retirements acknowledged by the delivery instance. + pub retirement_acknowledged: usize, + /// Inventory targets. + pub inventory: usize, + /// Hard artifact bytes reserved, including temporary and probe material. + pub reserved_bytes: u64, +} + +/// The durable store. +pub struct Store { + db: Database, + bounds: Bounds, + root: PathBuf, + /// Serializes filesystem ownership changes with retirement and cleanup. + objects: Mutex<()>, +} + +impl std::fmt::Debug for Store { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Store") + .field("bounds", &self.bounds) + .finish_non_exhaustive() + } +} + +fn encode(value: &T) -> Result, StoreError> { + serde_json::to_vec(value).map_err(storage) +} + +fn decode(bytes: &[u8]) -> Result { + serde_json::from_slice(bytes).map_err(storage) +} + +/// A target's job key. +#[must_use] +pub fn target_key(target: &Target) -> String { + format!("{}?{}", target.path, target.variant) +} + +fn outcome_key(target: &Target, identity: &str) -> String { + format!("{}#{identity}", target_key(target)) +} + +impl Store { + /// Opens or creates the store at `path` as its only owner. + /// + /// # Errors + /// + /// [`StoreError::AlreadyOwned`] where another process holds it, and + /// [`StoreError::InventoryFull`] where the persisted inventory exceeds + /// `bounds.max_inventory`; nothing is removed, and raising the bound + /// reopens it unchanged. + pub fn open(path: &Path, bounds: Bounds) -> Result { + if bounds.max_jobs == 0 + || bounds.reserved_jobs > bounds.max_jobs + || bounds.reserved_bytes > bounds.max_queue_bytes + || bounds.max_items_per_job == 0 + || bounds.max_attempts == 0 + || bounds.lease_seconds < 3 + { + return Err(storage("invalid worker store bounds")); + } + let db = Database::create(path).map_err(|error| match error { + redb::DatabaseError::DatabaseAlreadyOpen => StoreError::AlreadyOwned, + other => storage(other), + })?; + let root = path.parent().unwrap_or_else(|| Path::new(".")).to_owned(); + let store = Self { + db, + bounds, + root, + objects: Mutex::new(()), + }; + let tx = store.db.begin_write().map_err(storage)?; + for table in [ + JOBS, + OUTCOMES, + ARTIFACTS, + PLANS, + INVENTORY, + RETIRED, + OUTBOX, + SOURCES, + META, + TEXT_OPERATIONS, + PROBE_REFERENCES, + ] { + tx.open_table(table).map_err(storage)?; + } + tx.open_table(QUOTA).map_err(storage)?; + let inventory = tx + .open_table(INVENTORY) + .map_err(storage)? + .len() + .map_err(storage)? as usize; + if inventory > bounds.max_inventory { + tx.abort().map_err(storage)?; + return Err(StoreError::InventoryFull); + } + tx.commit().map_err(storage)?; + Ok(store) + } + + /// The bounds. + #[must_use] + pub fn bounds(&self) -> Bounds { + self.bounds + } + + fn read_value( + &self, + table: TableDefinition<&str, &[u8]>, + key: &str, + ) -> Result, StoreError> { + let tx = self.db.begin_read().map_err(storage)?; + let table = tx.open_table(table).map_err(storage)?; + table + .get(key) + .map_err(storage)? + .map(|value| decode(value.value())) + .transpose() + } + + fn read_all( + &self, + table: TableDefinition<&str, &[u8]>, + ) -> Result, StoreError> { + let tx = self.db.begin_read().map_err(storage)?; + let table = tx.open_table(table).map_err(storage)?; + let mut out = Vec::new(); + for entry in table.iter().map_err(storage)? { + let (key, value) = entry.map_err(storage)?; + out.push((key.value().to_owned(), decode(value.value())?)); + } + Ok(out) + } + + /// Admits a job for `target` in one transaction: drops retired, damped + /// and already published items, coalesces with the target's existing + /// job, and enforces the total and reserved bounds. A reader hint never + /// takes a reserved slot or byte. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn admit( + &self, + target: &Target, + origin: JobOrigin, + observed: &[ObservedItem], + now: u64, + ) -> Result { + let key = target_key(target); + let tx = self.db.begin_write().map_err(storage)?; + let admission = { + let mut jobs = tx.open_table(JOBS).map_err(storage)?; + let outcomes = tx.open_table(OUTCOMES).map_err(storage)?; + let retired = tx.open_table(RETIRED).map_err(storage)?; + if let Some(value) = outcomes + .get(outcome_key(target, "*").as_str()) + .map_err(storage)? + { + let outcome: Outcome = decode(value.value())?; + if outcome.class == OutcomeClass::Exhausted + && outcome.until > now + && origin != JobOrigin::Signal + { + return Ok(Admission::Damped); + } + } + let mut kept = Vec::new(); + for item in observed { + if retired + .get(item.identity.as_str()) + .map_err(storage)? + .is_some() + { + continue; + } + if let Some(outcome) = outcomes + .get(outcome_key(target, &item.identity).as_str()) + .map_err(storage)? + { + let outcome: Outcome = decode(outcome.value())?; + if now < outcome.until { + continue; + } + } + // An item already published but still missed by a reader was + // evicted from delivery storage; the job republishes its + // durable artifact and never signs it again. + if !kept.contains(item) { + kept.push(item.clone()); + } + } + let damped = origin == JobOrigin::Signal && kept.is_empty(); + let existing_job = if damped { + None + } else { + let existing = jobs.get(key.as_str()).map_err(storage)?; + existing + .map(|value| decode::(value.value())) + .transpose()? + }; + if damped { + Admission::Damped + } else if let Some(mut job) = existing_job { + // Existing work already covers reconciliation. A pending + // publisher warm will acquire current content itself; a reader + // cannot grow that job using the publisher's reserved bytes. + if origin == JobOrigin::Reconcile + || (origin == JobOrigin::Signal + && job.reserved + && matches!(job.state, JobState::Pending { .. })) + { + return Ok(Admission::Coalesced { key }); + } + let desired: Vec<_> = kept + .into_iter() + .take(self.bounds.max_items_per_job) + .collect(); + let desired_origin = origin; + if job.observed != desired || job.origin != desired_origin { + job.observed = desired; + job.origin = desired_origin; + job.reserved = origin != JobOrigin::Signal; + job.generation = job.generation.saturating_add(1); + job.follow_up |= matches!(job.state, JobState::Processing { .. }); + if !matches!(job.state, JobState::Processing { .. }) { + job.attempts = 0; + job.admitted_at = now; + job.state = JobState::Pending { ready_at: now }; + } + } + let encoded = encode(&job)?; + let mut total = encoded.len() + 128; + let mut readers = if job.reserved { 0 } else { encoded.len() + 128 }; + let mut reader_count = usize::from(!job.reserved); + for entry in jobs.iter().map_err(storage)? { + let (other_key, value) = entry.map_err(storage)?; + if other_key.value() == key { + continue; + } + total += value.value().len() + 128; + if !decode::(value.value())?.reserved { + readers += value.value().len() + 128; + reader_count += 1; + } + } + if total > self.bounds.max_queue_bytes + || (!job.reserved + && readers + > self + .bounds + .max_queue_bytes + .saturating_sub(self.bounds.reserved_bytes)) + || (!job.reserved + && reader_count + > self + .bounds + .max_jobs + .saturating_sub(self.bounds.reserved_jobs)) + { + return Ok(Admission::QueueFull); + } + jobs.insert(key.as_str(), encoded.as_slice()) + .map_err(storage)?; + Admission::Coalesced { key: key.clone() } + } else { + let mut count = 0_usize; + let mut bytes = 0_usize; + let mut reader_count = 0_usize; + let mut reader_bytes = 0_usize; + for entry in jobs.iter().map_err(storage)? { + let (_, value) = entry.map_err(storage)?; + let job: Job = decode(value.value())?; + count += 1; + bytes += value.value().len() + 128; + if !job.reserved { + reader_count += 1; + reader_bytes += value.value().len() + 128; + } + } + let job = Job { + key: key.clone(), + target: target.clone(), + origin, + observed: kept + .into_iter() + .take(self.bounds.max_items_per_job) + .collect(), + generation: 0, + follow_up: false, + attempts: 0, + admitted_at: now, + state: JobState::Pending { ready_at: now }, + reserved: origin != JobOrigin::Signal, + }; + let encoded = encode(&job)?; + let size = encoded.len() + 128; + let total_ok = + count < self.bounds.max_jobs && bytes + size <= self.bounds.max_queue_bytes; + let reader_ok = reader_count + < self + .bounds + .max_jobs + .saturating_sub(self.bounds.reserved_jobs) + && reader_bytes + size + <= self + .bounds + .max_queue_bytes + .saturating_sub(self.bounds.reserved_bytes); + if !total_ok || (origin == JobOrigin::Signal && !reader_ok) { + Admission::QueueFull + } else { + jobs.insert(key.as_str(), encoded.as_slice()) + .map_err(storage)?; + Admission::Queued { key: key.clone() } + } + } + }; + tx.commit().map_err(storage)?; + Ok(admission) + } + + /// Claims the next ready job, fencing it with a fresh token. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn claim(&self, now: u64, token: u64) -> Result, StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + let claimed = { + let mut jobs = tx.open_table(JOBS).map_err(storage)?; + let mut ready = None; + let mut exhausted = Vec::new(); + for entry in jobs.iter().map_err(storage)? { + let (key, value) = entry.map_err(storage)?; + let job: Job = decode(value.value())?; + if let JobState::Pending { ready_at } = job.state + && ready_at <= now + { + if job.attempts >= self.bounds.max_attempts + || now.saturating_sub(job.admitted_at) >= self.bounds.retry_window_seconds + { + exhausted.push((key.value().to_owned(), job)); + continue; + } + let older = ready.as_ref().is_none_or(|(_, prior): &(String, Job)| { + let prior_ready = match prior.state { + JobState::Pending { ready_at } => ready_at, + JobState::Processing { .. } => u64::MAX, + }; + (ready_at, job.admitted_at) < (prior_ready, prior.admitted_at) + }); + if older { + ready = Some((key.value().to_owned(), job)); + } + } + } + for (key, job) in exhausted { + damp_exhausted( + &tx, + &job, + "retry_window_exhausted", + now, + self.bounds.quota_bytes, + )?; + jobs.remove(key.as_str()).map_err(storage)?; + } + match ready { + Some((key, mut job)) => { + job.attempts += 1; + job.state = JobState::Processing { + token, + lease_until: now + self.bounds.lease_seconds, + }; + jobs.insert(key.as_str(), encode(&job)?.as_slice()) + .map_err(storage)?; + Some(Claim { job, token }) + } + None => None, + } + }; + tx.commit().map_err(storage)?; + Ok(claimed) + } + + /// Renews a claim's lease, only for its owner. Whether it still owns it. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn renew(&self, key: &str, token: u64, now: u64) -> Result { + self.with_claimed(key, token, |job| { + if matches!(job.state, JobState::Processing { lease_until, .. } if lease_until <= now) { + return Ok(None); + } + job.state = JobState::Processing { + token, + lease_until: now + self.bounds_lease(), + }; + Ok(Some(())) + }) + .map(|renewed| renewed.is_some()) + } + + fn bounds_lease(&self) -> u64 { + self.bounds.lease_seconds + } + + /// Applies `change` to a job still claimed under `token`; a stale claim + /// changes nothing. + fn with_claimed( + &self, + key: &str, + token: u64, + change: impl FnOnce(&mut Job) -> Result, StoreError>, + ) -> Result, StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + let result = { + let mut jobs = tx.open_table(JOBS).map_err(storage)?; + let current = jobs + .get(key) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + match current { + Some(mut job) if matches!(job.state, JobState::Processing { token: held, .. } if held == token) => + { + let result = change(&mut job)?; + jobs.insert(key, encode(&job)?.as_slice()) + .map_err(storage)?; + result + } + _ => None, + } + }; + tx.commit().map_err(storage)?; + Ok(result) + } + + /// Settles a claim that finished its work: removes the job, unless a + /// newer observation arrived, which is scheduled at once. A stale claim + /// settles nothing. Whether the claim was current. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn complete(&self, key: &str, token: u64, now: u64) -> Result { + let tx = self.db.begin_write().map_err(storage)?; + let current = { + let mut jobs = tx.open_table(JOBS).map_err(storage)?; + let job = jobs + .get(key) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + match job { + Some(mut job) if matches!(job.state, JobState::Processing { token: held, lease_until } if held == token && lease_until > now) => + { + if job.follow_up { + job.follow_up = false; + job.attempts = 0; + job.admitted_at = now; + job.state = JobState::Pending { ready_at: now }; + jobs.insert(key, encode(&job)?.as_slice()) + .map_err(storage)?; + } else { + jobs.remove(key).map_err(storage)?; + } + true + } + _ => false, + } + }; + tx.commit().map_err(storage)?; + Ok(current) + } + + /// Settles a claim whose attempt failed transiently: retried with + /// bounded exponential backoff while attempts and the retry window + /// last, else removed with each observed item damped for an hour. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn retry(&self, key: &str, token: u64, now: u64, code: &str) -> Result { + let tx = self.db.begin_write().map_err(storage)?; + { + let mut jobs = tx.open_table(JOBS).map_err(storage)?; + let current = jobs + .get(key) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + let Some(mut job) = current else { + return Ok(false); + }; + if !matches!(job.state, JobState::Processing { token: held, lease_until } if held == token && lease_until > now) + { + return Ok(false); + } + if job.follow_up { + // Failure of an older generation never damps or deletes the desired one. + job.follow_up = false; + job.attempts = 0; + job.admitted_at = now; + job.state = JobState::Pending { ready_at: now }; + jobs.insert(key, encode(&job)?.as_slice()) + .map_err(storage)?; + } else if job.attempts >= self.bounds.max_attempts + || now.saturating_sub(job.admitted_at) >= self.bounds.retry_window_seconds + { + damp_exhausted(&tx, &job, code, now, self.bounds.quota_bytes)?; + jobs.remove(key).map_err(storage)?; + } else { + let delay = 5_u64 + .saturating_mul(1_u64 << job.attempts.min(16)) + .min(self.bounds.max_backoff_seconds); + job.state = JobState::Pending { + ready_at: now.saturating_add(delay), + }; + jobs.insert(key, encode(&job)?.as_slice()) + .map_err(storage)?; + } + } + tx.commit().map_err(storage)?; + Ok(true) + } + + /// One job. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn job(&self, key: &str) -> Result, StoreError> { + self.read_value(JOBS, key) + } + + /// Returns every processing job whose lease ended to pending, as on + /// restart. How many were recovered. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn recover_expired(&self, now: u64) -> Result { + self.recover_claims(now, false) + } + + /// Recovers every claim at exclusive-owner startup, using a finite retry time. + /// + /// # Errors + /// A store transaction failure. + pub fn recover_all(&self, now: u64) -> Result { + self.recover_claims(now, true) + } + + fn recover_claims(&self, now: u64, all: bool) -> Result { + let tx = self.db.begin_write().map_err(storage)?; + let mut recovered = 0; + { + let mut jobs = tx.open_table(JOBS).map_err(storage)?; + let mut expired = Vec::new(); + for entry in jobs.iter().map_err(storage)? { + let (key, value) = entry.map_err(storage)?; + let job: Job = decode(value.value())?; + if let JobState::Processing { lease_until, .. } = job.state + && (all || lease_until <= now) + { + expired.push((key.value().to_owned(), job)); + } + } + for (key, mut job) in expired { + if job.follow_up { + job.follow_up = false; + job.attempts = 0; + job.admitted_at = now; + } else if job.attempts >= self.bounds.max_attempts + || now.saturating_sub(job.admitted_at) >= self.bounds.retry_window_seconds + { + damp_exhausted( + &tx, + &job, + "retry_window_exhausted", + now, + self.bounds.quota_bytes, + )?; + jobs.remove(key.as_str()).map_err(storage)?; + recovered += 1; + continue; + } + job.state = JobState::Pending { ready_at: now }; + jobs.insert(key.as_str(), encode(&job)?.as_slice()) + .map_err(storage)?; + recovered += 1; + } + } + tx.commit().map_err(storage)?; + Ok(recovered) + } + + /// Records one item's damped outcome. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn record_outcome( + &self, + target: &Target, + identity: &str, + code: &str, + class: OutcomeClass, + now: u64, + ) -> Result<(), StoreError> { + let until = match class { + OutcomeClass::SourceUnstable => now + 300, + OutcomeClass::Exhausted => now + 3600, + OutcomeClass::Terminal => u64::MAX, + }; + let outcome = Outcome { + code: code.to_owned(), + class, + until, + }; + let tx = self.db.begin_write().map_err(storage)?; + put_outcome(&tx, target, identity, &outcome, self.bounds.quota_bytes)?; + tx.commit().map_err(storage) + } + + /// Records an outcome only for the current claim and desired generation. + /// Late failures cannot damp an operator retry or a newer observation. + /// + /// # Errors + /// A failed store transaction. + pub fn record_claim_outcome( + &self, + claim: &Claim, + identity: &str, + code: &str, + class: OutcomeClass, + now: u64, + ) -> Result<(), StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + let current = tx + .open_table(JOBS) + .map_err(storage)? + .get(claim.job.key.as_str()) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + if current.is_some_and(|job| job.generation == claim.job.generation + && matches!(job.state, JobState::Processing { token, lease_until } if token == claim.token && lease_until > now)) + { + let until = match class { + OutcomeClass::SourceUnstable => now.saturating_add(300), + OutcomeClass::Exhausted => now.saturating_add(3600), + OutcomeClass::Terminal => u64::MAX, + }; + let outcome = Outcome { code: code.to_owned(), class, until }; + put_outcome(&tx, &claim.job.target, identity, &outcome, self.bounds.quota_bytes)?; + } + tx.commit().map_err(storage) + } + + /// Invalidates terminal damping when relevant configured behavior or the + /// authenticated probe release changes. Retirement tombstones are never cleared. + /// + /// # Errors + /// A failed store transaction. + pub fn refresh_context(&self, scope: &str, identity: &str) -> Result<(), StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + { + let mut meta = tx.open_table(META).map_err(storage)?; + let key = format!("damping_context:{scope}"); + let prior = meta + .get(key.as_str()) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + if prior.as_deref().is_some_and(|prior| prior != identity) { + let mut outcomes = tx.open_table(OUTCOMES).map_err(storage)?; + let mut stale = Vec::new(); + for entry in outcomes.iter().map_err(storage)? { + let (key, value) = entry.map_err(storage)?; + if decode::(value.value())?.class == OutcomeClass::Terminal { + stale.push(key.value().to_owned()); + } + } + for key in stale { + outcomes.remove(key.as_str()).map_err(storage)?; + tx.open_table(QUOTA) + .map_err(storage)? + .remove(format!("outcome:{key}").as_str()) + .map_err(storage)?; + } + } + meta.insert(key.as_str(), encode(&identity)?.as_slice()) + .map_err(storage)?; + } + tx.commit().map_err(storage) + } + + /// Clears every outcome of `target`, for an operator's explicit retry. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn clear_outcomes(&self, target: &Target, now: u64) -> Result<(), StoreError> { + let prefix = format!("{}#", target_key(target)); + let tx = self.db.begin_write().map_err(storage)?; + { + let mut outcomes = tx.open_table(OUTCOMES).map_err(storage)?; + let mut keys = Vec::new(); + for entry in outcomes.iter().map_err(storage)? { + let (key, _) = entry.map_err(storage)?; + if key.value().starts_with(&prefix) { + keys.push(key.value().to_owned()); + } + } + for key in keys { + outcomes.remove(key.as_str()).map_err(storage)?; + tx.open_table(QUOTA) + .map_err(storage)? + .remove(format!("outcome:{key}").as_str()) + .map_err(storage)?; + } + } + { + let mut jobs = tx.open_table(JOBS).map_err(storage)?; + let key = target_key(target); + let current = jobs + .get(key.as_str()) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + if let Some(mut job) = current { + job.generation = job.generation.saturating_add(1); + if matches!(job.state, JobState::Processing { .. }) { + job.follow_up = true; + } else { + job.attempts = 0; + job.admitted_at = now; + job.state = JobState::Pending { ready_at: now }; + } + jobs.insert(key.as_str(), encode(&job)?.as_slice()) + .map_err(storage)?; + } + } + tx.commit().map_err(storage) + } + + /// The outcome recorded for an item. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn outcome(&self, target: &Target, identity: &str) -> Result, StoreError> { + self.read_value(OUTCOMES, &outcome_key(target, identity)) + } + + /// Records a verified plan before its finalize, so a crash after + /// finalize recovers the receipt rather than planning again. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn record_plan(&self, identity: &str, planned: &PlannedMedia) -> Result<(), StoreError> { + let _guard = self.objects.lock().map_err(storage)?; + self.reserve_identity(identity)?; + let encoded = encode(planned)?; + if !self.reserve(&format!("plan:{identity}"), encoded.len() as u64)? { + return Err(StoreError::Quota); + } + let tx = self.db.begin_write().map_err(storage)?; + if tx + .open_table(RETIRED) + .map_err(storage)? + .get(identity) + .map_err(storage)? + .is_some() + { + return Err(StoreError::Storage( + "identity retired during planning".to_owned(), + )); + } + tx.open_table(PLANS) + .map_err(storage)? + .insert(identity, encoded.as_slice()) + .map_err(storage)?; + let mut references = tx.open_table(PROBE_REFERENCES).map_err(storage)?; + let sha = planned.release.sha256.as_str(); + let mut identities: Vec = references + .get(sha) + .map_err(storage)? + .map(|value| decode(value.value())) + .transpose()? + .unwrap_or_default(); + if !identities.iter().any(|existing| existing == identity) { + identities.push(identity.to_owned()); + } + let encoded = encode(&identities)?; + if !reserve_in( + &tx, + &format!("probe-record:{sha}"), + encoded.len() as u64, + self.bounds.quota_bytes, + )? { + return Err(StoreError::Quota); + } + references + .insert(sha, encoded.as_slice()) + .map_err(storage)?; + drop(references); + tx.commit().map_err(storage) + } + + /// The plan recorded for an identity. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn plan(&self, identity: &str) -> Result, StoreError> { + self.read_value(PLANS, identity) + } + + /// The distinct pinned releases retained plans require, without retaining all geometry in memory. + /// + /// # Errors + /// A failed store read. + pub fn probe_releases( + &self, + ) -> Result< + Vec, + StoreError, + > { + let tx = self.db.begin_read().map_err(storage)?; + let table = tx.open_table(PLANS).map_err(storage)?; + let mut releases = Vec::new(); + for entry in table.iter().map_err(storage)? { + let (_, value) = entry.map_err(storage)?; + let plan: PlannedMedia = decode(value.value())?; + if !releases.contains(&plan.release) { + releases.push(plan.release); + } + } + Ok(releases) + } + + /// Whether interrupted text operations can recover without creation capability. + /// + /// # Errors + /// A failed store read. + pub fn has_text_operations(&self) -> Result { + let tx = self.db.begin_read().map_err(storage)?; + Ok(tx + .open_table(TEXT_OPERATIONS) + .map_err(storage)? + .len() + .map_err(storage)? + > 0) + } + + /// Whether an identity owns reconstruction inputs or a durable output. + /// + /// # Errors + /// A failed store read. + pub fn has_reconstruction(&self, identity: &str) -> Result { + let tx = self.db.begin_read().map_err(storage)?; + for table in [PLANS, TEXT_OPERATIONS, ARTIFACTS] { + if tx + .open_table(table) + .map_err(storage)? + .get(identity) + .map_err(storage)? + .is_some() + { + return Ok(true); + } + } + Ok(self + .root + .join("artifacts") + .join(format!("{identity}.json")) + .exists()) + } + + /// Drops failed pre-plan source references without creating a retirement or spent key. + /// + /// # Errors + /// A failed store transaction or file removal; uncertain bytes remain charged. + pub fn discard_unplanned_sources(&self, identity: &str) -> Result<(), StoreError> { + let _guard = self.objects.lock().map_err(storage)?; + if self.has_reconstruction(identity)? { + return Ok(()); + } + let tx = self.db.begin_write().map_err(storage)?; + { + let mut sources = tx.open_table(SOURCES).map_err(storage)?; + let mut changed = Vec::new(); + for entry in sources.iter().map_err(storage)? { + let (sha, value) = entry.map_err(storage)?; + let mut source: SourceRecord = decode(value.value())?; + source.references.retain(|reference| reference != identity); + source.public &= !source.references.is_empty(); + changed.push((sha.value().to_owned(), source)); + } + for (sha, source) in changed { + sources + .insert(sha.as_str(), encode(&source)?.as_slice()) + .map_err(storage)?; + } + } + tx.commit().map_err(storage)?; + self.remove_unreferenced_sources()?; + if self.retirement(identity)?.is_none() { + self.release(&format!("identity:{identity}"))?; + } + Ok(()) + } + + /// Records a text attempt before its first network write. Returns false + /// for an existing operation, which must use recovery rather than signing. + /// + /// # Errors + /// Refuses retired identities, conflicting requests and exhausted storage. + pub fn begin_text( + &self, + identity: &str, + request: &trusted_server_provenance_encypher::preparation::pipeline::TextSignRequest, + ) -> Result { + let _guard = self.objects.lock().map_err(storage)?; + if self.retirement(identity)?.is_some() { + return Err(storage("text identity retired")); + } + self.reserve_identity(identity)?; + let file = self.write_text_file(identity, request)?; + let tx = self.db.begin_write().map_err(storage)?; + if tx + .open_table(RETIRED) + .map_err(storage)? + .get(identity) + .map_err(storage)? + .is_some() + { + return Err(storage("text identity retired")); + } + let created = { + let mut operations = tx.open_table(TEXT_OPERATIONS).map_err(storage)?; + let existing = operations + .get(identity) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + if let Some(existing) = existing { + if existing.sha256 != file.sha256 || existing.bytes != file.bytes { + return Err(storage("text operation conflict")); + } + false + } else { + operations + .insert(identity, encode(&file)?.as_slice()) + .map_err(storage)?; + true + } + }; + tx.commit().map_err(storage)?; + Ok(created) + } + + /// The identities retaining a pinned probe release, for lifecycle checks. + /// + /// # Errors + /// A store read failure. + pub fn probe_references(&self, sha256: &str) -> Result, StoreError> { + Ok(self + .read_value(PROBE_REFERENCES, sha256)? + .unwrap_or_default()) + } + + fn put( + &self, + table: TableDefinition<&str, &[u8]>, + key: &str, + value: &T, + ) -> Result<(), StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + tx.open_table(table) + .map_err(storage)? + .insert(key, encode(value)?.as_slice()) + .map_err(storage)?; + tx.commit().map_err(storage) + } + + /// Records a published artifact, unless its identity was retired after + /// it was prepared: a late result never outlives a retirement. + /// Whether it was recorded. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn record_artifact(&self, artifact: &Artifact) -> Result { + let identity = artifact.envelope.identity.as_str(); + let _guard = self.objects.lock().map_err(storage)?; + if self.retirement(identity)?.is_some() { + return Ok(false); + } + self.reserve_identity(identity)?; + // Stream JSON into an owned filesystem object, never a large redb value. + let file = self.write_artifact_file(identity, artifact)?; + let tx = self.db.begin_write().map_err(storage)?; + tx.open_table(ARTIFACTS) + .map_err(storage)? + .insert(identity, encode(&file)?.as_slice()) + .map_err(storage)?; + tx.commit().map_err(storage)?; + self.shrink_reservation( + &format!("artifact:{identity}"), + file.bytes + encode(&file)?.len() as u64, + )?; + Ok(true) + } + + /// The artifact of an identity. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn artifact(&self, identity: &str) -> Result, StoreError> { + let _guard = self.objects.lock().map_err(storage)?; + let Some(file) = self.read_value::(ARTIFACTS, identity)? else { + return Ok(None); + }; + self.read_artifact_file(identity, &file).map(Some) + } + + /// Records publication acknowledgement without rewriting immutable artifact bytes. + /// + /// # Errors + /// A failed store transaction. + pub fn acknowledge_artifact(&self, identity: &str) -> Result<(), StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + { + let mut artifacts = tx.open_table(ARTIFACTS).map_err(storage)?; + let current = artifacts + .get(identity) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + if let Some(mut file) = current { + file.acknowledged = true; + artifacts + .insert(identity, encode(&file)?.as_slice()) + .map_err(storage)?; + } + } + tx.commit().map_err(storage) + } + + /// The current retirement epoch. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn retirement_epoch(&self) -> Result { + Ok(self + .read_value::(META, "retirement_epoch")? + .unwrap_or(0)) + } + + /// Retires `identity` in one transaction: increments the epoch, records + /// the tombstone, drops its artifact and plan, and queues its marker in + /// the outbox before anything else is cleaned up. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn retire(&self, identity: &str, deployment_id: &str) -> Result { + let _guard = self.objects.lock().map_err(storage)?; + if let Some(retirement) = self.retirement(identity)? { + return Ok(retirement.epoch); + } + self.reserve_identity(identity)?; + let tx = self.db.begin_write().map_err(storage)?; + let epoch = { + let mut meta = tx.open_table(META).map_err(storage)?; + let epoch = meta + .get("retirement_epoch") + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()? + .unwrap_or(0) + + 1; + meta.insert("retirement_epoch", encode(&epoch)?.as_slice()) + .map_err(storage)?; + tx.open_table(RETIRED) + .map_err(storage)? + .insert( + identity, + encode(&Retirement { + epoch, + acknowledged: false, + })? + .as_slice(), + ) + .map_err(storage)?; + let marker = MarkerEnvelope { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: deployment_id.to_owned(), + identity: identity.to_owned(), + epoch, + }; + tx.open_table(OUTBOX) + .map_err(storage)? + .insert(identity, encode(&marker)?.as_slice()) + .map_err(storage)?; + tx.open_table(ARTIFACTS) + .map_err(storage)? + .remove(identity) + .map_err(storage)?; + tx.open_table(PLANS) + .map_err(storage)? + .remove(identity) + .map_err(storage)?; + tx.open_table(QUOTA) + .map_err(storage)? + .remove(format!("plan:{identity}").as_str()) + .map_err(storage)?; + tx.open_table(TEXT_OPERATIONS) + .map_err(storage)? + .remove(identity) + .map_err(storage)?; + let mut sources = tx.open_table(SOURCES).map_err(storage)?; + let mut changed = Vec::new(); + for entry in sources.iter().map_err(storage)? { + let (sha, value) = entry.map_err(storage)?; + let mut source: SourceRecord = decode(value.value())?; + source.references.retain(|reference| reference != identity); + source.public &= !source.references.is_empty(); + changed.push((sha.value().to_owned(), source)); + } + for (sha, source) in changed { + sources + .insert(sha.as_str(), encode(&source)?.as_slice()) + .map_err(storage)?; + } + drop(sources); + let mut probes = tx.open_table(PROBE_REFERENCES).map_err(storage)?; + let mut changed = Vec::new(); + for entry in probes.iter().map_err(storage)? { + let (sha, value) = entry.map_err(storage)?; + let mut references: Vec = decode(value.value())?; + references.retain(|reference| reference != identity); + changed.push((sha.value().to_owned(), references)); + } + for (sha, references) in changed { + probes + .insert(sha.as_str(), encode(&references)?.as_slice()) + .map_err(storage)?; + } + epoch + }; + tx.commit().map_err(storage)?; + // The tombstone and outbox commit first. A failed filesystem removal + // stays charged and can be retried safely on restart. + self.remove_artifact_file(identity)?; + self.remove_text_file(identity)?; + self.remove_unreferenced_sources()?; + Ok(epoch) + } + + /// Whether an identity is retired. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn retirement(&self, identity: &str) -> Result, StoreError> { + self.read_value(RETIRED, identity) + } + + /// Markers waiting for the delivery instance's acknowledgement. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn outbox(&self) -> Result, StoreError> { + Ok(self + .read_all::(OUTBOX)? + .into_iter() + .map(|(_, marker)| marker) + .collect()) + } + + /// A bounded round-robin marker batch; failed markers cannot starve later ones. + /// + /// # Errors + /// A failed cursor transaction or outbox read. + pub fn outbox_batch(&self, limit: usize) -> Result, StoreError> { + let cursor: String = self.read_value(META, "outbox_cursor")?.unwrap_or_default(); + let markers = self.outbox()?; + let batch: Vec<_> = markers + .iter() + .filter(|marker| marker.identity > cursor) + .chain(markers.iter().filter(|marker| marker.identity <= cursor)) + .take(limit.min(16)) + .cloned() + .collect(); + if let Some(last) = batch.last() { + self.put(META, "outbox_cursor", &last.identity)?; + } + Ok(batch) + } + + /// Records the delivery instance's acknowledgement of a marker at + /// `epoch`; an older epoch's acknowledgement settles nothing newer. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn acknowledge_marker(&self, identity: &str, epoch: u64) -> Result<(), StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + { + let mut retired = tx.open_table(RETIRED).map_err(storage)?; + let current = retired + .get(identity) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + if let Some(mut retirement) = current + && retirement.epoch == epoch + { + retirement.acknowledged = true; + retired + .insert(identity, encode(&retirement)?.as_slice()) + .map_err(storage)?; + tx.open_table(OUTBOX) + .map_err(storage)? + .remove(identity) + .map_err(storage)?; + } + } + tx.commit().map_err(storage) + } + + /// Registers inventory targets, within the bound. How many were added. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn register(&self, targets: &[Target]) -> Result { + let tx = self.db.begin_write().map_err(storage)?; + let mut added = 0; + { + let mut inventory = tx.open_table(INVENTORY).map_err(storage)?; + let existing = inventory.len().map_err(storage)? as usize; + for target in targets { + let key = target_key(target); + if inventory.get(key.as_str()).map_err(storage)?.is_some() { + continue; + } + if existing + added >= self.bounds.max_inventory { + return Err(StoreError::InventoryFull); + } + let entry = InventoryEntry { + target: target.clone(), + last_scan: 0, + last_identities: Vec::new(), + }; + let encoded = encode(&entry)?; + if !reserve_in( + &tx, + &format!("inventory:{key}"), + encoded.len() as u64, + self.bounds.quota_bytes, + )? { + return Err(StoreError::Quota); + } + inventory + .insert(key.as_str(), encoded.as_slice()) + .map_err(storage)?; + added += 1; + } + } + tx.commit().map_err(storage)?; + Ok(added) + } + + /// Up to `limit` inventory targets after the persisted cursor whose last + /// reconciliation is older than `interval`, advancing the cursor. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn due_inventory( + &self, + now: u64, + interval: u64, + limit: usize, + ) -> Result, StoreError> { + let cursor: String = self + .read_value(META, "inventory_cursor")? + .unwrap_or_default(); + let entries = self.read_all::(INVENTORY)?; + let limit = limit.min(128); + if limit == 0 { + return Ok(Vec::new()); + } + // Leave one cursor revolution of slack so a target cannot miss the + // configured maximum merely by falling just before the scan cursor. + let threshold = + interval.saturating_sub((entries.len().div_ceil(limit) as u64).saturating_mul(60)); + let mut ordered: Vec<&(String, InventoryEntry)> = + entries.iter().filter(|(key, _)| *key > cursor).collect(); + ordered.extend(entries.iter().filter(|(key, _)| *key <= cursor)); + let mut due = Vec::new(); + let mut last = cursor.clone(); + for (key, entry) in ordered.into_iter().take(limit) { + last.clone_from(key); + if entry.last_scan == 0 || now.saturating_sub(entry.last_scan) >= threshold { + due.push(entry.target.clone()); + } + } + self.put(META, "inventory_cursor", &last)?; + Ok(due) + } + + fn reserve_identity(&self, identity: &str) -> Result<(), StoreError> { + // This survives artifact deletion and funds the spent key and immutable + // retirement outbox even when the rest of the hard quota is full. + if self.reserve(&format!("identity:{identity}"), 1024)? { + Ok(()) + } else { + Err(StoreError::Quota) + } + } + + /// The identities of a target's last snapshot, and records this one's. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn observe_snapshot( + &self, + target: &Target, + identities: &[String], + now: u64, + ) -> Result>, StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + let previous = Self::observe_in(&tx, target, identities, now, self.bounds.quota_bytes)?; + tx.commit().map_err(storage)?; + Ok(previous) + } + + /// Observes an admitted snapshot only for the live claim and desired generation. + /// A stale completion cannot overwrite stability evidence for its follow-up. + /// + /// # Errors + /// A failed transaction. + pub fn observe_claim_snapshot( + &self, + claim: &Claim, + identities: &[String], + now: u64, + ) -> Result>, StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + let current = tx + .open_table(JOBS) + .map_err(storage)? + .get(claim.job.key.as_str()) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + if !current.is_some_and(|job| job.generation == claim.job.generation + && matches!(job.state, JobState::Processing { token, lease_until } if token == claim.token && lease_until > now)) + { + return Ok(None); + } + { + let mut outcomes = tx.open_table(OUTCOMES).map_err(storage)?; + let key = outcome_key(&claim.job.target, "*"); + let terminal = outcomes + .get(key.as_str()) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()? + .is_some_and(|outcome| outcome.class == OutcomeClass::Terminal); + if terminal { + outcomes.remove(key.as_str()).map_err(storage)?; + tx.open_table(QUOTA) + .map_err(storage)? + .remove(format!("outcome:{key}").as_str()) + .map_err(storage)?; + } + } + let previous = Self::observe_in( + &tx, + &claim.job.target, + identities, + now, + self.bounds.quota_bytes, + )?; + tx.commit().map_err(storage)?; + Ok(previous) + } + + fn observe_in( + tx: &redb::WriteTransaction, + target: &Target, + identities: &[String], + now: u64, + limit: u64, + ) -> Result>, StoreError> { + let key = target_key(target); + let mut inventory = tx.open_table(INVENTORY).map_err(storage)?; + let current = inventory + .get(key.as_str()) + .map_err(storage)? + .map(|value| decode::(value.value())) + .transpose()?; + let Some(mut entry) = current else { + return Ok(None); + }; + let previous = std::mem::replace(&mut entry.last_identities, identities.to_vec()); + entry.last_scan = now; + let encoded = encode(&entry)?; + if !reserve_in(tx, &format!("inventory:{key}"), encoded.len() as u64, limit)? { + return Err(StoreError::Quota); + } + inventory + .insert(key.as_str(), encoded.as_slice()) + .map_err(storage)?; + Ok(Some(previous)) + } + + /// Records a source snapshot, public only after page and asset + /// admission, referenced by `identity`. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn record_source( + &self, + sha256: &str, + mime: &str, + public: bool, + identity: &str, + ) -> Result<(), StoreError> { + let _guard = self.objects.lock().map_err(storage)?; + self.reserve_identity(identity)?; + self.record_source_locked(sha256, mime, public, identity) + } + + fn record_source_locked( + &self, + sha256: &str, + mime: &str, + public: bool, + identity: &str, + ) -> Result<(), StoreError> { + if self.retirement(identity)?.is_some() { + return Err(storage("source owner retired")); + } + let tx = self.db.begin_write().map_err(storage)?; + { + let mut sources = tx.open_table(SOURCES).map_err(storage)?; + let mut record: SourceRecord = sources + .get(sha256) + .map_err(storage)? + .map(|value| decode(value.value())) + .transpose()? + .unwrap_or(SourceRecord { + mime: mime.to_owned(), + public, + references: Vec::new(), + }); + if record.mime != mime { + return Err(storage("source media type conflict")); + } + record.public |= public; + if !record + .references + .iter() + .any(|existing| existing == identity) + { + record.references.push(identity.to_owned()); + } + let encoded = encode(&record)?; + if !reserve_in( + &tx, + &format!("source-record:{sha256}"), + encoded.len() as u64, + self.bounds.quota_bytes, + )? { + return Err(StoreError::Quota); + } + sources + .insert(sha256, encoded.as_slice()) + .map_err(storage)?; + } + tx.commit().map_err(storage) + } + + /// A source snapshot's record. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn source(&self, sha256: &str) -> Result, StoreError> { + self.read_value(SOURCES, sha256) + } + + /// Counts for status. + /// + /// # Errors + /// + /// A [`StoreError`] where a read fails. + pub fn counts(&self) -> Result { + let tx = self.db.begin_read().map_err(storage)?; + let mut counts = Counts::default(); + for entry in tx + .open_table(JOBS) + .map_err(storage)? + .iter() + .map_err(storage)? + { + let (_, value) = entry.map_err(storage)?; + match decode::(value.value())?.state { + JobState::Pending { .. } => counts.pending += 1, + JobState::Processing { .. } => counts.processing += 1, + } + } + for entry in tx + .open_table(ARTIFACTS) + .map_err(storage)? + .iter() + .map_err(storage)? + { + let (_, value) = entry.map_err(storage)?; + counts.artifacts += 1; + if decode::(value.value())?.acknowledged { + counts.publication_acknowledged += 1; + } else { + counts.publication_pending += 1; + } + } + for entry in tx + .open_table(RETIRED) + .map_err(storage)? + .iter() + .map_err(storage)? + { + let (_, value) = entry.map_err(storage)?; + if decode::(value.value())?.acknowledged { + counts.retirement_acknowledged += 1; + } else { + counts.retirement_pending += 1; + } + } + counts.inventory = usize::try_from( + tx.open_table(INVENTORY) + .map_err(storage)? + .len() + .map_err(storage)?, + ) + .map_err(storage)?; + for entry in tx + .open_table(QUOTA) + .map_err(storage)? + .iter() + .map_err(storage)? + { + counts.reserved_bytes = counts + .reserved_bytes + .saturating_add(entry.map_err(storage)?.1.value()); + } + Ok(counts) + } +} + +impl Store { + /// Reserves `bytes` for `owner` in one transaction, against every + /// reservation every task holds. Whether it fit: a full quota refuses + /// the work rather than evicting a retained result. An owner that + /// already holds a reservation keeps the larger of the two. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn reserve(&self, owner: &str, bytes: u64) -> Result { + let tx = self.db.begin_write().map_err(storage)?; + let fits = reserve_in(&tx, owner, bytes, self.bounds.quota_bytes)?; + tx.commit().map_err(storage)?; + Ok(fits) + } + + /// Returns conservative surplus after its durable object has been measured. + /// + /// # Errors + /// Refuses growth or a failed transaction. + pub fn shrink_reservation(&self, owner: &str, bytes: u64) -> Result<(), StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + { + let mut quota = tx.open_table(QUOTA).map_err(storage)?; + let held = quota + .get(owner) + .map_err(storage)? + .map_or(0, |value| value.value()); + if held < bytes { + return Err(storage("cannot grow by shrinking reservation")); + } + quota.insert(owner, bytes).map_err(storage)?; + } + tx.commit().map_err(storage) + } + + /// Releases `owner`'s reservation. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + pub fn release(&self, owner: &str) -> Result<(), StoreError> { + let tx = self.db.begin_write().map_err(storage)?; + tx.open_table(QUOTA) + .map_err(storage)? + .remove(owner) + .map_err(storage)?; + tx.commit().map_err(storage) + } + + fn reservation(&self, owner: &str) -> Result, StoreError> { + let tx = self.db.begin_read().map_err(storage)?; + Ok(tx + .open_table(QUOTA) + .map_err(storage)? + .get(owner) + .map_err(storage)? + .map(|value| value.value())) + } + + /// The bytes reserved now. + /// + /// # Errors + /// + /// A [`StoreError`] where the read fails. + pub fn reserved(&self) -> Result { + let tx = self.db.begin_read().map_err(storage)?; + let quota = tx.open_table(QUOTA).map_err(storage)?; + let mut used = 0_u64; + for entry in quota.iter().map_err(storage)? { + used = used.saturating_add(entry.map_err(storage)?.1.value()); + } + Ok(used) + } + + /// Reconciles reservations on restart: every temporary reservation is + /// released, since no task survives a restart and its file is removed + /// with it, and every artifact reservation whose artifact is gone, as + /// after a retirement, is released. Retained material keeps its bytes. + /// How many were released. + /// + /// # Errors + /// + /// A [`StoreError`] where the transaction fails. + fn reconcile_reservations(&self) -> Result { + let tx = self.db.begin_write().map_err(storage)?; + let released = { + let artifacts = tx.open_table(ARTIFACTS).map_err(storage)?; + let plans = tx.open_table(PLANS).map_err(storage)?; + let text = tx.open_table(TEXT_OPERATIONS).map_err(storage)?; + let sources = tx.open_table(SOURCES).map_err(storage)?; + let probes = tx.open_table(PROBE_REFERENCES).map_err(storage)?; + let retired = tx.open_table(RETIRED).map_err(storage)?; + let mut quota = tx.open_table(QUOTA).map_err(storage)?; + let mut stale = Vec::new(); + for entry in quota.iter().map_err(storage)? { + let (key, _) = entry.map_err(storage)?; + let key = key.value().to_owned(); + let orphan = match key.split_once(':') { + Some(("temp", _)) => true, + Some(("artifact", identity)) => { + artifacts.get(identity).map_err(storage)?.is_none() + && !self + .root + .join("artifacts") + .join(format!("{identity}.json")) + .try_exists() + .map_err(storage)? + && plans.get(identity).map_err(storage)?.is_none() + && text.get(identity).map_err(storage)?.is_none() + } + Some(("identity", identity)) if is_digest(identity) => { + retired.get(identity).map_err(storage)?.is_none() + && artifacts.get(identity).map_err(storage)?.is_none() + && plans.get(identity).map_err(storage)?.is_none() + && text.get(identity).map_err(storage)?.is_none() + && !self + .root + .join("artifacts") + .join(format!("{identity}.json")) + .try_exists() + .map_err(storage)? + } + Some(("text", identity)) if is_digest(identity) => { + text.get(identity).map_err(storage)?.is_none() + && !self + .root + .join("texts") + .join(format!("{identity}.json")) + .try_exists() + .map_err(storage)? + } + Some(("source", digest)) if is_digest(digest) => { + sources.get(digest).map_err(storage)?.is_none() + && !self + .root + .join("sources") + .join(digest) + .try_exists() + .map_err(storage)? + } + Some(("probe", digest)) if is_digest(digest) => { + probes.get(digest).map_err(storage)?.is_none() + && !self + .root + .join("probes") + .join(format!("{digest}.wasm")) + .try_exists() + .map_err(storage)? + } + _ => false, + }; + if orphan { + stale.push(key); + } + } + for key in &stale { + quota.remove(key.as_str()).map_err(storage)?; + } + stale.len() + }; + tx.commit().map_err(storage)?; + Ok(released) + } +} + +/// How a pipeline failure damps the item. +#[must_use] +pub fn outcome_class(class: FailureClass) -> Option { + match class { + FailureClass::Transient | FailureClass::PolicyChanged | FailureClass::Missing => None, + FailureClass::Integrity + | FailureClass::Privacy + | FailureClass::Unsupported + | FailureClass::ExistingProvenance => Some(OutcomeClass::Terminal), + } +} + +#[cfg(test)] +mod tests { + use trusted_server_provenance_encypher::preparation::pipeline::TextCacheScope; + use trusted_server_provenance_encypher::preparation::protocol::ItemKind; + + use super::*; + + fn target(path: &str) -> Target { + Target { + path: path.to_owned(), + variant: String::new(), + } + } + + fn item(identity: &str) -> ObservedItem { + ObservedItem { + kind: ItemKind::Embedded, + identity: identity.repeat(64 / identity.len()), + } + } + + fn open(dir: &tempfile::TempDir, bounds: Bounds) -> Store { + Store::open(&dir.path().join("prep.redb"), bounds).expect("should open the store") + } + + #[test] + fn quota_reservations_are_shared_hard_and_reconciled_after_restart() { + let dir = tempfile::tempdir().expect("should make a directory"); + let bounds = Bounds { + quota_bytes: 100, + ..Bounds::default() + }; + { + let store = open(&dir, bounds); + assert!( + store.reserve("source:a", 40).expect("reserve"), + "should fit" + ); + assert!(store.reserve("temp:b", 50).expect("reserve"), "should fit"); + assert!( + !store.reserve("artifact:c", 20).expect("reserve"), + "a reservation past the quota is refused, never squeezed in" + ); + assert!( + store.reserve("source:a", 30).expect("reserve"), + "an owner keeps the larger of its reservations" + ); + assert_eq!(store.reserved().expect("read"), 90); + assert!(store.reserve("artifact:c", 10).expect("reserve")); + } + let store = open(&dir, bounds); + assert_eq!( + store.reconcile_reservations().expect("reconcile"), + 2, + "a temporary reservation and an artifact reservation without its artifact are released" + ); + assert_eq!( + store.reserved().expect("read"), + 40, + "retained source bytes stay reserved across a restart" + ); + } + + #[test] + fn a_second_owner_cannot_open_the_store() { + let dir = tempfile::tempdir().expect("should make a directory"); + let _owner = open(&dir, Bounds::default()); + let second = Store::open(&dir.path().join("prep.redb"), Bounds::default()); + assert_eq!( + second.expect_err("should refuse a second owner"), + StoreError::AlreadyOwned + ); + } + + #[test] + fn identical_hints_coalesce_and_a_hint_during_processing_schedules_one_follow_up() { + let dir = tempfile::tempdir().expect("should make a directory"); + let store = open(&dir, Bounds::default()); + let page = target("/news/a"); + assert_eq!( + store + .admit(&page, JobOrigin::Signal, &[item("a")], 10) + .expect("admit"), + Admission::Queued { + key: target_key(&page) + } + ); + assert_eq!( + store + .admit(&page, JobOrigin::Signal, &[item("a")], 11) + .expect("admit"), + Admission::Coalesced { + key: target_key(&page) + }, + "an identical hint should coalesce" + ); + let claim = store.claim(12, 7).expect("claim").expect("should claim"); + store + .admit(&page, JobOrigin::Signal, &[item("b")], 13) + .expect("admit"); + assert!(store.complete(&claim.job.key, 7, 14).expect("complete")); + let job = store + .job(&claim.job.key) + .expect("read") + .expect("should keep a follow-up"); + assert!( + matches!(job.state, JobState::Pending { .. }), + "follow-up is pending" + ); + assert!(!job.follow_up, "exactly one follow-up is scheduled"); + assert!( + job.observed.contains(&item("b")), + "the newer observation is kept" + ); + } + + #[test] + fn a_stale_claim_cannot_settle_or_renew_a_newer_one() { + let dir = tempfile::tempdir().expect("should make a directory"); + let store = open(&dir, Bounds::default()); + let page = target("/news/a"); + store.admit(&page, JobOrigin::Warm, &[], 0).expect("admit"); + let first = store.claim(0, 1).expect("claim").expect("should claim"); + assert_eq!( + store.recover_expired(1000).expect("recover"), + 1, + "expired lease returns" + ); + let second = store + .claim(1000, 2) + .expect("claim") + .expect("should reclaim"); + assert!( + !store.complete(&first.job.key, 1, 1001).expect("complete"), + "stale token" + ); + assert!( + !store.renew(&first.job.key, 1, 1001).expect("renew"), + "stale renewal" + ); + assert!( + store.job(&second.job.key).expect("read").is_some(), + "newer claim survives" + ); + assert!(store.complete(&second.job.key, 2, 1002).expect("complete")); + } + + #[test] + fn reader_hints_cannot_take_reserved_capacity() { + let dir = tempfile::tempdir().expect("should make a directory"); + let bounds = Bounds { + max_jobs: 3, + reserved_jobs: 1, + ..Bounds::default() + }; + let store = open(&dir, bounds); + for index in 0..2 { + let admitted = store + .admit( + &target(&format!("/r/{index}")), + JobOrigin::Signal, + &[item("a")], + 0, + ) + .expect("admit"); + assert!(matches!(admitted, Admission::Queued { .. })); + } + assert_eq!( + store + .admit(&target("/r/9"), JobOrigin::Signal, &[item("a")], 0) + .expect("admit"), + Admission::QueueFull, + "a hint should not take the reserved slot" + ); + assert!( + matches!( + store + .admit(&target("/w/1"), JobOrigin::Warm, &[], 0) + .expect("admit"), + Admission::Queued { .. } + ), + "publication work should use the reserved slot" + ); + } + + #[test] + fn damping_is_per_item_and_retirement_survives_restart() { + let dir = tempfile::tempdir().expect("should make a directory"); + let page = target("/news/a"); + { + let store = open(&dir, Bounds::default()); + store + .record_outcome( + &page, + &item("a").identity, + "source_unstable", + OutcomeClass::SourceUnstable, + 100, + ) + .expect("record"); + assert_eq!( + store + .admit(&page, JobOrigin::Signal, &[item("a")], 101) + .expect("admit"), + Admission::Damped, + "the unstable item is damped" + ); + assert!( + matches!( + store + .admit(&page, JobOrigin::Signal, &[item("a"), item("b")], 102) + .expect("admit"), + Admission::Queued { .. } + ), + "its stable sibling still queues" + ); + assert_eq!( + store + .retire(&item("c").identity, "deployment") + .expect("retire"), + 1 + ); + } + let store = open(&dir, Bounds::default()); + assert!( + store + .retirement(&item("c").identity) + .expect("read") + .is_some() + ); + assert_eq!( + store.outbox().expect("outbox").len(), + 1, + "the marker stays queued" + ); + store + .acknowledge_marker(&item("c").identity, 1) + .expect("ack"); + assert!(store.outbox().expect("outbox").is_empty()); + let artifact = Artifact { + envelope: ResultEnvelope { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: "deployment".to_owned(), + identity: item("c").identity, + retirement_epoch: 0, + variant: String::new(), + item: trusted_server_provenance_encypher::preparation::protocol::EnvelopeItem::Text { + request: trusted_server_provenance_encypher::preparation::pipeline::TextSignRequest { + text: "t".to_owned(), + page_url: "https://www.example.com/news/a".to_owned(), + document_title: None, + boundary_selector: "article".to_owned(), + expected_policy_digest: "a".repeat(64), + cache_scope: TextCacheScope::ExactRequest, + }, + idempotency_key: "k".to_owned(), + }, + }, + target: page.clone(), + acknowledged: true, + }; + assert!( + !store.record_artifact(&artifact).expect("record"), + "a late result never outlives its retirement" + ); + } + + #[test] + fn crash_after_artifact_rename_before_metadata_commit_adopts_exact_old_epoch() { + let dir = tempfile::tempdir().expect("should create durable storage"); + let identity = "a".repeat(64); + let artifact = Artifact { + envelope: ResultEnvelope { + protocol: PREPARATION_PROTOCOL.to_owned(), deployment_id: "deployment".to_owned(), + identity: identity.clone(), retirement_epoch: 0, variant: String::new(), + item: trusted_server_provenance_encypher::preparation::protocol::EnvelopeItem::Text { + request: trusted_server_provenance_encypher::preparation::pipeline::TextSignRequest { + text: "durable exact text".to_owned(), page_url: "https://www.example.com/article".to_owned(), + document_title: None, boundary_selector: "article".to_owned(), expected_policy_digest: "b".repeat(64), + cache_scope: TextCacheScope::ExactRequest, + }, + idempotency_key: "exact-operation".to_owned(), + }, + }, + target: target("/article"), acknowledged: false, + }; + { + let store = open(&dir, Bounds::default()); + { + let _guard = store + .objects + .lock() + .expect("should own filesystem mutation"); + store + .reserve_identity(&identity) + .expect("should fund immutable identity"); + store + .write_artifact_file(&identity, &artifact) + .expect("should fsync and rename real artifact"); + // Simulate death before ARTIFACTS metadata insertion, exactly + // between the two durable stages of record_artifact. + } + assert!( + store + .artifact(&identity) + .expect("should lack metadata before recovery") + .is_none() + ); + store + .retire(&"c".repeat(64), "deployment") + .expect("should advance unrelated retirement epoch"); + } + let store = open(&dir, Bounds::default()); + store + .recover_storage() + .expect("should adopt owned orphan after restart"); + let restored = store + .artifact(&identity) + .expect("should read durable result") + .expect("should recover completed artifact"); + assert_eq!( + restored.envelope, artifact.envelope, + "recovery preserves exact operation, bytes and old epoch" + ); + assert_eq!( + store + .retirement_epoch() + .expect("should retain unrelated retirement"), + 1 + ); + assert_eq!(restored.envelope.retirement_epoch, 0); + assert!( + store + .reserved() + .expect("should retain artifact and spent-key charges") + > 2048 + ); + } +} diff --git a/crates/trusted-server-preparation-worker/src/store_files.rs b/crates/trusted-server-preparation-worker/src/store_files.rs new file mode 100644 index 0000000000..bfea5ffa70 --- /dev/null +++ b/crates/trusted-server-preparation-worker/src/store_files.rs @@ -0,0 +1,549 @@ +//! Filesystem ownership of quota-charged objects. All mutations hold `Store::objects`. +use std::collections::HashSet; +use std::fs::{File, OpenOptions}; +use std::io::{Read as _, Write}; +use std::path::Path; + +use redb::{ReadableDatabase as _, ReadableTable as _}; +use serde::Serialize; +use sha2::{Digest as _, Sha256}; + +use super::{ + Artifact, ArtifactFile, PLANS, PROBE_REFERENCES, RETIRED, SOURCES, SourceRecord, Store, + StoreError, TEXT_OPERATIONS, encode, storage, +}; +use trusted_server_provenance_encypher::preparation::protocol::is_digest; + +struct Measure { + bytes: u64, + hash: Sha256, +} + +impl Default for Measure { + fn default() -> Self { + Self { + bytes: 0, + hash: Sha256::new(), + } + } +} + +impl Write for Measure { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.bytes = self + .bytes + .checked_add(bytes.len() as u64) + .ok_or_else(|| std::io::Error::other("artifact size overflow"))?; + self.hash.update(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +fn hash_file(path: &Path) -> Result<(u64, String), StoreError> { + let mut input = File::open(path).map_err(storage)?; + let mut measure = Measure::default(); + let mut buffer = [0; 64 * 1024]; + loop { + let read = input.read(&mut buffer).map_err(storage)?; + if read == 0 { + break; + } + measure.write_all(&buffer[..read]).map_err(storage)?; + } + Ok((measure.bytes, crate::hex_lower(&measure.hash.finalize()))) +} + +fn sync_directory(path: &Path) -> Result<(), StoreError> { + File::open(path) + .and_then(|file| file.sync_all()) + .map_err(storage) +} + +impl Store { + fn object_path(&self, kind: &str, digest: &str) -> Result { + if !is_digest(digest) || !matches!(kind, "source" | "probe" | "artifact" | "text") { + return Err(StoreError::Storage("invalid object identity".to_owned())); + } + let (directory, suffix) = match kind { + "source" => ("sources", ""), + "probe" => ("probes", ".wasm"), + "text" => ("texts", ".json"), + _ => ("artifacts", ".json"), + }; + Ok(self.root.join(directory).join(format!("{digest}{suffix}"))) + } + + /// Keeps digest-addressed source or probe bytes, with a durable hard reservation. + /// + /// # Errors + /// Refuses invalid bytes, quota exhaustion or filesystem failure. + pub fn keep_blob(&self, kind: &str, digest: &str, bytes: &[u8]) -> Result<(), StoreError> { + let _guard = self.objects.lock().map_err(storage)?; + self.keep_blob_locked(kind, digest, bytes) + } + + fn keep_blob_locked(&self, kind: &str, digest: &str, bytes: &[u8]) -> Result<(), StoreError> { + let path = self.object_path(kind, digest)?; + if crate::hex_lower(&Sha256::digest(bytes)) != digest { + return Err(StoreError::Storage("object digest mismatch".to_owned())); + } + self.write_object(kind, digest, &path, bytes.len() as u64, |file| { + file.write_all(bytes).map_err(storage) + }) + } + + /// Keeps public source bytes and their owner reference under one retirement guard. + /// + /// # Errors + /// A retired owner, invalid source, full quota or storage failure. + pub fn keep_source( + &self, + digest: &str, + mime: &str, + bytes: &[u8], + identity: &str, + ) -> Result<(), StoreError> { + let _guard = self.objects.lock().map_err(storage)?; + if self.retirement(identity)?.is_some() { + return Err(storage("source owner retired")); + } + self.reserve_identity(identity)?; + self.keep_blob_locked("source", digest, bytes)?; + self.record_source_locked(digest, mime, true, identity) + } + + /// Reads and checks a retained source or probe object without releasing its charge on error. + /// + /// # Errors + /// Missing/corrupt files and invalid identities refuse explicitly. + pub fn blob(&self, kind: &str, digest: &str) -> Result, StoreError> { + let _guard = self.objects.lock().map_err(storage)?; + let path = self.object_path(kind, digest)?; + let bytes = std::fs::read(path).map_err(storage)?; + if crate::hex_lower(&Sha256::digest(&bytes)) != digest { + return Err(StoreError::Storage( + "retained object digest mismatch".to_owned(), + )); + } + Ok(bytes) + } + + fn write_object( + &self, + kind: &str, + identity: &str, + path: &Path, + bytes: u64, + write: impl FnOnce(&mut File) -> Result<(), StoreError>, + ) -> Result<(), StoreError> { + let owner = format!("{kind}:{identity}"); + if !self.reserve(&owner, bytes)? { + return Err(StoreError::Quota); + } + let parent = path + .parent() + .ok_or_else(|| storage("missing object directory"))?; + std::fs::create_dir_all(parent).map_err(storage)?; + // A second writer never creates a second copy of an already valid object. + if matches!(kind, "source" | "probe") + && path.exists() + && hash_file(path)? == (bytes, identity.to_owned()) + { + return Ok(()); + } + // A replacement must charge both the old file and its temporary replacement. + let temporary_owner = format!("temp:replace:{kind}:{identity}"); + let replacement = path.exists(); + if replacement && !self.reserve(&temporary_owner, bytes)? { + return Err(StoreError::Quota); + } + let pending = path.with_extension("pending"); + let result = (|| { + let mut file = OpenOptions::new() + .create_new(true) + .write(true) + .open(&pending) + .map_err(storage)?; + write(&mut file)?; + file.sync_all().map_err(storage)?; + std::fs::rename(&pending, path).map_err(storage)?; + sync_directory(parent) + })(); + if result.is_err() && pending.exists() { + // Do not release accounting until removal is confirmed. + std::fs::remove_file(&pending).map_err(storage)?; + sync_directory(parent)?; + } + if replacement { + self.release(&temporary_owner)?; + } + result + } + + pub(super) fn write_artifact_file( + &self, + identity: &str, + artifact: &Artifact, + ) -> Result { + self.write_json_file("artifact", identity, artifact, artifact.acknowledged) + } + + pub(super) fn write_text_file( + &self, + identity: &str, + request: &trusted_server_provenance_encypher::preparation::pipeline::TextSignRequest, + ) -> Result { + self.write_json_file("text", identity, request, false) + } + + fn write_json_file( + &self, + kind: &str, + identity: &str, + value: &T, + acknowledged: bool, + ) -> Result { + let mut measure = Measure::default(); + serde_json::to_writer(&mut measure, value).map_err(storage)?; + let record = ArtifactFile { + bytes: measure.bytes, + sha256: crate::hex_lower(&measure.hash.finalize()), + acknowledged, + }; + let path = self.object_path(kind, identity)?; + if path.exists() { + let (bytes, digest) = hash_file(&path)?; + if bytes != record.bytes || digest != record.sha256 { + return Err(storage("immutable artifact conflict")); + } + if !self.reserve( + &format!("{kind}:{identity}"), + record.bytes + encode(&record)?.len() as u64, + )? { + return Err(StoreError::Quota); + } + return Ok(record); + } + self.write_object( + kind, + identity, + &path, + record.bytes + encode(&record)?.len() as u64, + |file| serde_json::to_writer(file, value).map_err(storage), + )?; + Ok(record) + } + + pub(super) fn read_artifact_file( + &self, + identity: &str, + record: &ArtifactFile, + ) -> Result { + let path = self.object_path("artifact", identity)?; + if hash_file(&path)? != (record.bytes, record.sha256.clone()) { + return Err(storage("retained artifact digest mismatch")); + } + let mut artifact: Artifact = + serde_json::from_reader(File::open(path).map_err(storage)?).map_err(storage)?; + if artifact.envelope.identity != identity { + return Err(storage("retained artifact identity mismatch")); + } + artifact.acknowledged = record.acknowledged; + Ok(artifact) + } + + /// Removes crash-only temporary files before releasing their ownership records. + /// Retained objects, including probes written by the earlier unmetered cache, + /// are measured and charged before the worker can report ready. + /// + /// # Errors + /// Any unreadable directory, unsafe entry, deletion failure or existing quota excess blocks startup. + pub fn recover_storage(&self) -> Result<(), StoreError> { + let _guard = self.objects.lock().map_err(storage)?; + // A committed retirement remains authoritative if file cleanup was interrupted. + for (identity, _) in self.read_all::(RETIRED)? { + self.remove_artifact_file(&identity)?; + self.remove_text_file(&identity)?; + self.reserve_identity(&identity)?; + } + // No task survives exclusive-owner restart. References without a + // persisted plan/result never reached finalize and can be reacquired. + let tx = self.db.begin_write().map_err(storage)?; + { + let mut sources = tx.open_table(SOURCES).map_err(storage)?; + let mut changed = Vec::new(); + for entry in sources.iter().map_err(storage)? { + let (sha, value) = entry.map_err(storage)?; + let mut source: SourceRecord = super::decode(value.value())?; + let mut kept = Vec::new(); + for identity in &source.references { + if self.has_reconstruction(identity)? { + kept.push(identity.clone()); + } + } + source.references = kept; + source.public &= !source.references.is_empty(); + changed.push((sha.value().to_owned(), source)); + } + for (sha, source) in changed { + sources + .insert(sha.as_str(), encode(&source)?.as_slice()) + .map_err(storage)?; + } + } + tx.commit().map_err(storage)?; + self.remove_unreferenced_sources()?; + let mut planned_sources = HashSet::new(); + { + let tx = self.db.begin_read().map_err(storage)?; + let plans = tx.open_table(PLANS).map_err(storage)?; + for entry in plans.iter().map_err(storage)? { + let (_, value) = entry.map_err(storage)?; + let plan: trusted_server_provenance_encypher::preparation::pipeline::PlannedMedia = + super::decode(value.value())?; + planned_sources.insert(plan.request.source.sha256); + } + } + let directories = [ + ("sources", "source", ""), + ("probes", "probe", ".wasm"), + ("artifacts", "artifact", ".json"), + ("texts", "text", ".json"), + ("large", "temp", ""), + ]; + // Remove owned crash-only objects before releasing their durable reservations. + for (directory, kind, _) in directories { + let dir = self.root.join(directory); + std::fs::create_dir_all(&dir).map_err(storage)?; + let metadata = std::fs::symlink_metadata(&dir).map_err(storage)?; + if !metadata.is_dir() || metadata.file_type().is_symlink() { + return Err(storage("unsafe object directory")); + } + for entry in std::fs::read_dir(&dir).map_err(storage)? { + let entry = entry.map_err(storage)?; + if !entry.file_type().map_err(storage)?.is_file() { + return Err(storage("unsafe object entry")); + } + let name = entry + .file_name() + .into_string() + .map_err(|_| storage("invalid object filename"))?; + if directory == "large" { + if self.reservation(&format!("temp:large:{name}"))?.is_none() + && self.reservation(&format!("temp:{name}"))?.is_none() + { + return Err(storage("temporary source has no durable owner")); + } + std::fs::remove_file(entry.path()).map_err(storage)?; + } else if name.ends_with(".pending") || name.contains(".tmp-") { + let digest = name + .split('.') + .next() + .ok_or_else(|| storage("invalid temporary object"))?; + if !is_digest(digest) + || self.reservation(&format!("{kind}:{digest}"))?.is_none() + { + return Err(storage("temporary object has no durable owner")); + } + std::fs::remove_file(entry.path()).map_err(storage)?; + } else if directory == "sources" + && is_digest(&name) + && self.source(&name)?.is_none() + && !planned_sources.contains(&name) + { + std::fs::remove_file(entry.path()).map_err(storage)?; + sync_directory(&dir)?; + self.release(&format!("source:{name}"))?; + } else if directory == "texts" { + let identity = name + .strip_suffix(".json") + .ok_or_else(|| storage("invalid text filename"))?; + if !is_digest(identity) { + return Err(storage("invalid text filename")); + } + // Without the metadata commit, begin_text never returned and + // this owner could not have sent the signing request. + if self + .read_value::(TEXT_OPERATIONS, identity)? + .is_none() + { + std::fs::remove_file(entry.path()).map_err(storage)?; + sync_directory(&dir)?; + self.release(&format!("text:{identity}"))?; + } + } + } + sync_directory(&dir)?; + } + self.reconcile_reservations()?; + // Historical unmetered probes and every retained filesystem object are charged. + for (directory, kind, suffix) in directories { + if directory == "large" { + continue; + } + for entry in std::fs::read_dir(self.root.join(directory)).map_err(storage)? { + let entry = entry.map_err(storage)?; + let name = entry + .file_name() + .into_string() + .map_err(|_| storage("invalid object filename"))?; + let digest = name + .strip_suffix(suffix) + .ok_or_else(|| storage("unknown object filename"))?; + if !is_digest(digest) { + return Err(storage("unknown object filename")); + } + if !self.reserve( + &format!("{kind}:{digest}"), + entry.metadata().map_err(storage)?.len(), + )? { + return Err(StoreError::Quota); + } + } + } + for (kind, table) in [ + ("plan", PLANS), + ("source-record", SOURCES), + ("probe-record", PROBE_REFERENCES), + ("inventory", super::INVENTORY), + ("outcome", super::OUTCOMES), + ] { + for (identity, value) in self.read_all::(table)? { + if kind == "plan" { + self.reserve_identity(&identity)?; + } + let bytes = encode(&value)?.len() as u64 + + if kind == "outcome" { + identity.len() as u64 + } else { + 0 + }; + if !self.reserve(&format!("{kind}:{identity}"), bytes)? { + return Err(StoreError::Quota); + } + } + } + // A crash may follow fsync+rename but precede the metadata transaction. + // Adopt that exact object instead of rebuilding it under a later + // retirement epoch or asking the API to sign it again. + for entry in std::fs::read_dir(self.root.join("artifacts")).map_err(storage)? { + let entry = entry.map_err(storage)?; + let name = entry + .file_name() + .into_string() + .map_err(|_| storage("invalid artifact filename"))?; + let identity = name + .strip_suffix(".json") + .ok_or_else(|| storage("invalid artifact filename"))?; + if self + .read_value::(super::ARTIFACTS, identity)? + .is_some() + { + continue; + } + let (bytes, sha256) = hash_file(&entry.path())?; + let file = ArtifactFile { + bytes, + sha256, + acknowledged: false, + }; + let artifact = self.read_artifact_file(identity, &file)?; + if artifact.envelope.protocol + != trusted_server_provenance_encypher::preparation::protocol::PREPARATION_PROTOCOL + || !artifact.target.is_well_formed() + || self.retirement(identity)?.is_some() + { + return Err(storage("orphan artifact identity invalid")); + } + if !self.reserve( + &format!("artifact:{identity}"), + bytes + encode(&file)?.len() as u64, + )? { + return Err(StoreError::Quota); + } + self.put(super::ARTIFACTS, identity, &file)?; + } + for (identity, file) in self.read_all::(super::ARTIFACTS)? { + self.read_artifact_file(&identity, &file)?; + self.reserve_identity(&identity)?; + if !self.reserve( + &format!("artifact:{identity}"), + file.bytes + encode(&file)?.len() as u64, + )? { + return Err(StoreError::Quota); + } + } + for (identity, file) in self.read_all::(TEXT_OPERATIONS)? { + self.reserve_identity(&identity)?; + if hash_file(&self.object_path("text", &identity)?)? + != (file.bytes, file.sha256.clone()) + { + return Err(storage("retained text operation digest mismatch")); + } + if !self.reserve( + &format!("text:{identity}"), + file.bytes + encode(&file)?.len() as u64, + )? { + return Err(StoreError::Quota); + } + } + for (sha, source) in self.read_all::(SOURCES)? { + if !source.references.is_empty() { + let path = self.object_path("source", &sha)?; + if hash_file(&path)?.1 != sha { + return Err(storage("retained public source corrupt")); + } + } + } + Ok(()) + } + + pub(super) fn remove_artifact_file(&self, identity: &str) -> Result<(), StoreError> { + let path = self.object_path("artifact", identity)?; + match std::fs::remove_file(&path) { + Ok(()) => sync_directory( + path.parent() + .ok_or_else(|| storage("missing artifact directory"))?, + )?, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(storage(error)), + } + self.release(&format!("artifact:{identity}")) + } + + pub(super) fn remove_text_file(&self, identity: &str) -> Result<(), StoreError> { + let path = self.object_path("text", identity)?; + match std::fs::remove_file(&path) { + Ok(()) => sync_directory( + path.parent() + .ok_or_else(|| storage("missing text directory"))?, + )?, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(storage(error)), + } + self.release(&format!("text:{identity}")) + } + + pub(super) fn remove_unreferenced_sources(&self) -> Result<(), StoreError> { + for (sha, source) in self.read_all::(SOURCES)? { + if !source.references.is_empty() { + continue; + } + let path = self.object_path("source", &sha)?; + match std::fs::remove_file(&path) { + Ok(()) => sync_directory( + path.parent() + .ok_or_else(|| storage("missing source directory"))?, + )?, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(storage(error)), + } + self.release(&format!("source:{sha}"))?; + // Keep the tiny nonpublic record as a durable admission tombstone. + } + Ok(()) + } +} diff --git a/crates/trusted-server-preparation-worker/src/store_maintenance.rs b/crates/trusted-server-preparation-worker/src/store_maintenance.rs new file mode 100644 index 0000000000..11474df695 --- /dev/null +++ b/crates/trusted-server-preparation-worker/src/store_maintenance.rs @@ -0,0 +1,81 @@ +//! Bounded maintenance of quota-charged negative outcomes. +use std::collections::BTreeMap; + +use redb::{ReadableDatabase as _, ReadableTable as _}; + +use super::{ + META, OUTCOMES, Outcome, OutcomeClass, QUOTA, Store, StoreError, decode, encode, storage, +}; + +impl Store { + /// Reclaims at most 256 expired outcomes in a cursor-bounded scan. + /// Terminal outcomes and retirement tombstones are never time-evicted. + /// + /// # Errors + /// A failed store transaction. Accounting is released with row deletion. + pub fn prune_expired_outcomes(&self, now: u64) -> Result { + let tx = self.db.begin_write().map_err(storage)?; + let cursor: String = tx + .open_table(META) + .map_err(storage)? + .get("outcome_prune_cursor") + .map_err(storage)? + .map(|value| decode(value.value())) + .transpose()? + .unwrap_or_default(); + let mut next = String::new(); + let mut stale = Vec::new(); + { + let mut outcomes = tx.open_table(OUTCOMES).map_err(storage)?; + let mut scanned = 0; + let mut last = String::new(); + for entry in outcomes.range(cursor.as_str()..).map_err(storage)? { + let (key, value) = entry.map_err(storage)?; + if key.value() <= cursor.as_str() { + continue; + } + if scanned == 256 { + next = last; + break; + } + last = key.value().to_owned(); + scanned += 1; + let outcome: Outcome = decode(value.value())?; + if outcome.class != OutcomeClass::Terminal && outcome.until <= now { + stale.push(key.value().to_owned()); + } + } + for key in &stale { + outcomes.remove(key.as_str()).map_err(storage)?; + tx.open_table(QUOTA) + .map_err(storage)? + .remove(format!("outcome:{key}").as_str()) + .map_err(storage)?; + } + } + tx.open_table(META) + .map_err(storage)? + .insert("outcome_prune_cursor", encode(&next)?.as_slice()) + .map_err(storage)?; + tx.commit().map_err(storage)?; + Ok(stale.len()) + } + + /// Counts current per-item/target refusals by stable code for authenticated status. + /// + /// # Errors + /// A failed store read. + pub fn active_outcome_counts(&self, now: u64) -> Result, StoreError> { + let tx = self.db.begin_read().map_err(storage)?; + let table = tx.open_table(OUTCOMES).map_err(storage)?; + let mut counts = BTreeMap::new(); + for entry in table.iter().map_err(storage)? { + let (_, value) = entry.map_err(storage)?; + let outcome: Outcome = decode(value.value())?; + if outcome.until > now { + *counts.entry(outcome.code).or_default() += 1; + } + } + Ok(counts) + } +} diff --git a/crates/trusted-server-preparation-worker/tests/durable_storage.rs b/crates/trusted-server-preparation-worker/tests/durable_storage.rs new file mode 100644 index 0000000000..232471ae44 --- /dev/null +++ b/crates/trusted-server-preparation-worker/tests/durable_storage.rs @@ -0,0 +1,488 @@ +//! Real redb/filesystem scenarios. Probe bytes here exercise storage, not probe execution. +use std::sync::{Arc, Barrier}; + +use bytes::Bytes; +use sha2::{Digest as _, Sha256}; +use trusted_server_preparation_worker::client::DiskProbeCache; +use trusted_server_preparation_worker::store::{Bounds, Store}; +use trusted_server_provenance_encypher::preparation::pipeline::{ + ProbeArtifactCache, TextCacheScope, TextSignRequest, +}; + +fn digest(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +#[test] +fn four_tasks_cannot_overcommit_probe_files_and_restart_keeps_their_charge() { + let dir = tempfile::tempdir().expect("should create storage"); + let probe = Bytes::from( + std::fs::read( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../provenance/encypher/src/fixtures/prepared/probe.wasm"), + ) + .expect("should read the common real probe"), + ); + let probe_bytes = probe.len() as u64; + let bounds = Bounds { + quota_bytes: probe_bytes + 300, + ..Bounds::default() + }; + { + let store = Arc::new( + Store::open(&dir.path().join("preparation.redb"), bounds).expect("should open"), + ); + let barrier = Arc::new(Barrier::new(4)); + let tasks: Vec<_> = (0..4) + .map(|index| { + let store = Arc::clone(&store); + let barrier = Arc::clone(&barrier); + let bytes = probe.clone(); + std::thread::spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("should build runtime"); + let cache = DiskProbeCache::new(Arc::clone(&store)); + let sha = digest(&bytes); + runtime + .block_on(cache.put(&sha, bytes)) + .expect("should retain shared real probe"); + barrier.wait(); + store + .reserve(&format!("temp:task:{index}"), 100) + .expect("should reserve shared capacity") + }) + }) + .collect(); + let admitted = tasks + .into_iter() + .map(|task| task.join().expect("should join")) + .filter(|admitted| *admitted) + .count(); + assert_eq!( + admitted, 3, + "real probe plus three task budgets exactly reaches the quota; the fourth refuses" + ); + assert_eq!(store.reserved().expect("should count"), probe_bytes + 300); + assert!(!store.reserve("temp:fourth", 1).expect("should reserve")); + } + let store = Store::open(&dir.path().join("preparation.redb"), bounds).expect("should reopen"); + store + .recover_storage() + .expect("should reconcile real files"); + assert_eq!( + store.reserved().expect("should count"), + probe_bytes, + "restart reclaims dead task budgets, never the retained real probe" + ); +} + +#[tokio::test] +async fn real_probe_cache_refuses_before_writing_when_its_durable_reservation_cannot_fit() { + let dir = tempfile::tempdir().expect("should create storage"); + let probe = Bytes::from( + std::fs::read( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../provenance/encypher/src/fixtures/prepared/probe.wasm"), + ) + .expect("should read common real probe"), + ); + let sha = digest(&probe); + let store = Arc::new( + Store::open( + &dir.path().join("preparation.redb"), + Bounds { + quota_bytes: probe.len() as u64 - 1, + ..Bounds::default() + }, + ) + .expect("should open"), + ); + let cache = DiskProbeCache::new(Arc::clone(&store)); + let failure = cache + .put(&sha, probe) + .await + .expect_err("should refuse unreserved probe retention"); + assert_eq!(failure.code, "quota_exhausted"); + assert!(cache.get(&sha).await.is_none()); + assert_eq!(store.reserved().expect("should count"), 0); +} + +#[tokio::test] +async fn duplicate_probe_writes_are_charged_once_and_corrupt_cache_is_not_returned() { + let dir = tempfile::tempdir().expect("should create storage"); + let store = Arc::new( + Store::open( + &dir.path().join("preparation.redb"), + Bounds { + quota_bytes: 10, + ..Bounds::default() + }, + ) + .expect("should open"), + ); + let cache = DiskProbeCache::new(Arc::clone(&store)); + let bytes = Bytes::from_static(b"probe-data"); + let sha = digest(&bytes); + cache + .put(&sha, bytes.clone()) + .await + .expect("should retain probe"); + cache + .put(&sha, bytes.clone()) + .await + .expect("should reuse probe"); + assert_eq!(cache.get(&sha).await, Some(bytes)); + assert_eq!(store.reserved().expect("should count"), 10); + std::fs::write( + dir.path().join("probes").join(format!("{sha}.wasm")), + b"tampered!!", + ) + .expect("should tamper"); + assert!( + cache.get(&sha).await.is_none(), + "digest-addressed files must verify on read" + ); + assert_eq!( + store.reserved().expect("should count"), + 10, + "corruption does not release a file's charge" + ); +} + +#[test] +fn restart_accounts_legacy_unreserved_probe_and_preserves_overquota_evidence() { + let dir = tempfile::tempdir().expect("should create storage"); + std::fs::create_dir(dir.path().join("probes")).expect("should create probes"); + let bytes = b"retained probe"; + let path = dir + .path() + .join("probes") + .join(format!("{}.wasm", digest(bytes))); + std::fs::write(&path, bytes).expect("should write legacy probe"); + let store = Store::open( + &dir.path().join("preparation.redb"), + Bounds { + quota_bytes: 5, + ..Bounds::default() + }, + ) + .expect("should open"); + assert!( + store.recover_storage().is_err(), + "cannot report ready with historical bytes over quota" + ); + assert_eq!( + std::fs::read(path).expect("should preserve retained file"), + bytes + ); +} + +#[test] +fn restart_releases_temporary_ownership_only_after_removing_its_actual_file() { + let dir = tempfile::tempdir().expect("should create storage"); + let bounds = Bounds { + quota_bytes: 100, + ..Bounds::default() + }; + { + let store = Store::open(&dir.path().join("preparation.redb"), bounds).expect("should open"); + assert!( + store + .reserve("temp:large:dead-claim", 80) + .expect("should reserve") + ); + std::fs::create_dir(dir.path().join("large")).expect("should create temporary directory"); + std::fs::write(dir.path().join("large/dead-claim"), [0; 80]) + .expect("should write temporary source"); + } + let store = Store::open(&dir.path().join("preparation.redb"), bounds).expect("should reopen"); + store + .recover_storage() + .expect("should reclaim inactive temporary source"); + assert!(!dir.path().join("large/dead-claim").exists()); + assert_eq!(store.reserved().expect("should count"), 0); + assert!( + store + .reserve("temp:new", 100) + .expect("should reserve reclaimed capacity") + ); +} + +#[test] +fn restart_refuses_legacy_or_unknown_text_scope_in_retained_publication() { + for scope in [None, Some(serde_json::json!("legacy"))] { + let dir = tempfile::tempdir().expect("should create storage"); + let path = dir.path().join("preparation.redb"); + let identity = "a".repeat(64); + let mut artifact = serde_json::json!({ + "envelope": { + "protocol": "encypher-preparation/1", + "deployment_id": "deployment", + "identity": identity, + "retirement_epoch": 0, + "variant": "", + "item": { + "kind": "text", + "request": { + "text": "A retained article", + "page_url": "https://www.example.com/article", + "document_title": "Harbor", + "boundary_selector": "article", + "expected_policy_digest": "b".repeat(64), + }, + "idempotency_key": "retained-operation", + }, + }, + "target": {"path": "/article", "variant": ""}, + "acknowledged": false, + }); + if let Some(scope) = scope { + artifact["envelope"]["item"]["request"]["cache_scope"] = scope; + } + { + let store = Store::open(&path, Bounds::default()).expect("should open storage"); + store + .recover_storage() + .expect("should initialize object directories"); + std::fs::write( + dir.path() + .join("artifacts") + .join(format!("{identity}.json")), + serde_json::to_vec(&artifact).expect("should encode the retained publication"), + ) + .expect("should retain a pre-cutover artifact at the filesystem boundary"); + } + let store = Store::open(&path, Bounds::default()).expect("should reopen storage"); + assert!( + store.recover_storage().is_err(), + "unsupported scope must block recovery, never adopt a legacy publication" + ); + assert!( + store + .artifact(&identity) + .expect("should read metadata") + .is_none(), + "unsupported text must not become a publishable retained result" + ); + } +} + +#[test] +fn large_text_attempt_is_streamed_retained_and_retired_without_losing_its_spent_key() { + let dir = tempfile::tempdir().expect("should create storage"); + let bounds = Bounds { + quota_bytes: 3 * 1024 * 1024, + ..Bounds::default() + }; + let identity = "a".repeat(64); + let request = TextSignRequest { + text: "a".repeat(2 * 1024 * 1024), + page_url: "https://www.example.com/article".to_owned(), + document_title: None, + boundary_selector: "article".to_owned(), + expected_policy_digest: "b".repeat(64), + cache_scope: TextCacheScope::ExactRequest, + }; + { + let store = + Store::open(&dir.path().join("preparation.redb"), bounds).expect("should own store"); + assert!( + store + .begin_text(&identity, &request) + .expect("should persist before first signing attempt") + ); + let path = dir.path().join("texts").join(format!("{identity}.json")); + let persisted: serde_json::Value = serde_json::from_slice( + &std::fs::read(&path).expect("should retain the exact text request"), + ) + .expect("should store a JSON request"); + assert_eq!(persisted["cache_scope"], "exact_request"); + assert_eq!( + serde_json::from_value::(persisted) + .expect("should recover the current cache contract"), + request + ); + assert!( + std::fs::metadata(path) + .expect("should stream a filesystem object") + .len() + > request.text.len() as u64 + ); + assert!( + store.reserved().expect("should charge retained input") > request.text.len() as u64 + ); + } + let store = Store::open(&dir.path().join("preparation.redb"), bounds).expect("should reopen"); + store + .recover_storage() + .expect("should retain reconstruction inputs across restart"); + assert!( + !store + .begin_text(&identity, &request) + .expect("should require non-signing recovery for an existing attempt") + ); + let mut substituted = request.clone(); + substituted.text.push('x'); + assert!( + store.begin_text(&identity, &substituted).is_err(), + "one identity cannot change its frozen request" + ); + let epoch = store + .retire(&identity, "deployment") + .expect("should retire through durable owner"); + assert!( + !dir.path() + .join("texts") + .join(format!("{identity}.json")) + .exists() + ); + assert_eq!( + store + .reserved() + .expect("should keep funded spent-key material"), + 1024 + ); + let marker = store + .outbox() + .expect("should commit delayed marker with spent key") + .pop() + .expect("should retain marker"); + assert_eq!(marker.epoch, epoch); + drop(store); + let store = Store::open(&dir.path().join("preparation.redb"), bounds) + .expect("should reopen after retirement"); + store + .recover_storage() + .expect("should preserve retirement while reconciling real files"); + assert_eq!( + store + .reserved() + .expect("should preserve funded spent key across restart"), + 1024 + ); + assert_eq!( + store + .outbox() + .expect("should retain pending marker across restart"), + vec![marker] + ); + let retirement = store + .retirement(&identity) + .expect("should read spent key") + .expect("should preserve tombstone"); + assert_eq!(retirement.epoch, epoch); + assert!(!retirement.acknowledged); + assert!( + store.begin_text(&identity, &request).is_err(), + "retirement must refuse the original signing request after restart" + ); + assert!( + store.begin_text(&identity, &substituted).is_err(), + "a changed request must not reuse the retired operation identity" + ); + assert!( + !dir.path() + .join("texts") + .join(format!("{identity}.json")) + .exists(), + "refused resurrection must not recreate the retained request" + ); + assert_eq!( + store + .reserved() + .expect("should not allocate a fresh signing operation"), + 1024 + ); +} + +#[test] +fn ambiguous_temporary_file_blocks_cleanup_without_releasing_any_reservation() { + let dir = tempfile::tempdir().expect("should create storage"); + let store = Store::open( + &dir.path().join("preparation.redb"), + Bounds { + quota_bytes: 100, + ..Bounds::default() + }, + ) + .expect("should open"); + store + .reserve("temp:known", 80) + .expect("should reserve known ownership"); + std::fs::create_dir(dir.path().join("large")).expect("should create temporary directory"); + let path = dir.path().join("large/ambiguous"); + std::fs::write(&path, b"unowned evidence").expect("should create uncertain crash evidence"); + assert!( + store.recover_storage().is_err(), + "unowned material must not be deleted to manufacture quota" + ); + assert!(path.exists()); + assert_eq!( + store + .reserved() + .expect("should preserve ownership accounting"), + 80 + ); +} + +#[test] +fn expired_negative_outcomes_release_their_charge_but_terminal_outcomes_do_not_time_expire() { + let dir = tempfile::tempdir().expect("should create storage"); + let store = + Store::open(&dir.path().join("preparation.redb"), Bounds::default()).expect("should open"); + let target = trusted_server_provenance_encypher::preparation::protocol::Target { + path: "/article".to_owned(), + variant: String::new(), + }; + store + .record_outcome( + &target, + &"a".repeat(64), + "source_unstable", + trusted_server_preparation_worker::store::OutcomeClass::SourceUnstable, + 0, + ) + .expect("should retain bounded negative state"); + store + .record_outcome( + &target, + &"b".repeat(64), + "source_private", + trusted_server_preparation_worker::store::OutcomeClass::Terminal, + 0, + ) + .expect("should retain terminal state"); + let before = store + .reserved() + .expect("should charge all retained outcome bytes"); + assert_eq!( + store + .prune_expired_outcomes(301) + .expect("should reclaim only expired state"), + 1 + ); + assert!( + store + .reserved() + .expect("should release expired bytes atomically") + < before + ); + assert_eq!( + store + .active_outcome_counts(301) + .expect("should expose refusal reasons") + .get("source_private"), + Some(&1) + ); + assert!( + store + .outcome(&target, &"b".repeat(64)) + .expect("should preserve terminal suppression") + .is_some() + ); +} diff --git a/crates/trusted-server-preparation-worker/tests/queue_lifecycle.rs b/crates/trusted-server-preparation-worker/tests/queue_lifecycle.rs new file mode 100644 index 0000000000..0e433516eb --- /dev/null +++ b/crates/trusted-server-preparation-worker/tests/queue_lifecycle.rs @@ -0,0 +1,808 @@ +//! Transactional scenarios use the real owner, durable queue and filesystem. +use std::sync::{Arc, Barrier}; + +use trusted_server_preparation_worker::store::{ + Admission, Artifact, Bounds, JobOrigin, JobState, OutcomeClass, Store, StoreError, target_key, +}; +use trusted_server_provenance_encypher::preparation::pipeline::{TextCacheScope, TextSignRequest}; +use trusted_server_provenance_encypher::preparation::protocol::{ + EnvelopeItem, ItemKind, ObservedItem, PREPARATION_PROTOCOL, ResultEnvelope, Target, +}; + +fn target(index: usize) -> Target { + Target { + path: format!("/article/{index:04}"), + variant: String::new(), + } +} +fn item(byte: char) -> ObservedItem { + ObservedItem { + identity: byte.to_string().repeat(64), + kind: ItemKind::Text, + } +} + +#[test] +fn identical_processing_hint_does_not_schedule_signing_churn_and_latest_replaces_history() { + let dir = tempfile::tempdir().expect("should create store"); + let store = Store::open(&dir.path().join("preparation.redb"), Bounds::default()) + .expect("should own store"); + let target = target(1); + store + .admit(&target, JobOrigin::Signal, &[item('a')], 1) + .expect("should admit"); + let first = store + .claim(1, 10) + .expect("should claim") + .expect("should have work"); + store + .admit(&target, JobOrigin::Signal, &[item('a')], 2) + .expect("should coalesce duplicate"); + assert!( + !store + .job(&first.job.key) + .expect("should read") + .expect("should remain claimed") + .follow_up + ); + for byte in ['b', 'c', 'd'] { + store + .admit(&target, JobOrigin::Signal, &[item(byte)], 3) + .expect("should replace desired generation"); + } + let desired = store + .job(&first.job.key) + .expect("should read") + .expect("should retain desired state"); + assert_eq!( + desired.observed, + vec![item('d')], + "one latest desired observation, not history that fills 64 slots" + ); + assert_eq!( + desired.generation, + first.job.generation + 3, + "each changed observation advances the desired version" + ); + store + .retry(&first.job.key, first.token, 4, "old_failure") + .expect("should settle old claim"); + let pending = store + .job(&first.job.key) + .expect("should read pending follow-up") + .expect("should preserve latest generation"); + assert!( + !store + .complete(&first.job.key, first.token, 4) + .expect("should refuse duplicate old completion") + ); + assert_eq!( + store + .job(&first.job.key) + .expect("should read after stale completion"), + Some(pending), + "a duplicate old success cannot clear the desired pending version" + ); + let next = store + .claim(4, 11) + .expect("should claim immediately") + .expect("should keep follow-up despite old failure"); + assert_eq!(next.job.observed, vec![item('d')]); + assert_eq!(next.job.generation, desired.generation); + assert_eq!(next.job.attempts, 1); + assert!( + !store + .complete(&first.job.key, first.token, 5) + .expect("should refuse stale completion") + ); + assert!( + store + .complete(&next.job.key, next.token, 5) + .expect("should settle latest generation") + ); +} + +#[test] +fn exhausted_old_claim_cannot_remove_new_desired_generation() { + let dir = tempfile::tempdir().expect("should create store"); + let store = Store::open( + &dir.path().join("preparation.redb"), + Bounds { + max_attempts: 1, + ..Bounds::default() + }, + ) + .expect("should open"); + let page = target(1); + store + .admit(&page, JobOrigin::Signal, &[item('a')], 0) + .expect("should admit"); + let first = store + .claim(0, 1) + .expect("should claim") + .expect("should have work"); + store + .admit(&page, JobOrigin::Signal, &[item('b')], 1) + .expect("should retain desired change"); + store + .retry(&first.job.key, first.token, 2, "offline") + .expect("should settle old attempt"); + let latest = store + .claim(2, 2) + .expect("should claim") + .expect("should keep latest despite exhausted old attempt"); + assert_eq!(latest.job.observed, vec![item('b')]); + assert!( + store + .outcome(&page, &item('b').identity) + .expect("should read") + .is_none() + ); +} + +#[test] +fn coalescing_cannot_exceed_the_metadata_reservation() { + let dir = tempfile::tempdir().expect("should create store"); + let bounds = Bounds { + max_queue_bytes: 1400, + reserved_bytes: 600, + max_jobs: 3, + reserved_jobs: 1, + ..Bounds::default() + }; + let store = Store::open(&dir.path().join("preparation.redb"), bounds).expect("should open"); + let page = target(1); + assert!(matches!( + store + .admit(&page, JobOrigin::Signal, &[item('a')], 0) + .expect("should admit"), + Admission::Queued { .. } + )); + let large: Vec<_> = ('a'..='f').map(item).collect(); + assert_eq!( + store + .admit(&page, JobOrigin::Signal, &large, 1) + .expect("should bound replacement"), + Admission::QueueFull + ); + assert_eq!( + store + .job(&target_key(&page)) + .expect("should read") + .expect("should preserve old job") + .observed, + vec![item('a')] + ); + assert!(matches!( + store + .admit(&target(2), JobOrigin::Warm, &[], 1) + .expect("should preserve publisher reservation"), + Admission::Queued { .. } + )); +} + +#[test] +fn concurrent_inventory_registration_never_silently_exceeds_or_partially_claims_capacity() { + let dir = tempfile::tempdir().expect("should create store"); + let store = Arc::new( + Store::open( + &dir.path().join("preparation.redb"), + Bounds { + max_inventory: 2, + ..Bounds::default() + }, + ) + .expect("should open"), + ); + let barrier = Arc::new(Barrier::new(4)); + let handles: Vec<_> = (0..4) + .map(|index| { + let store = Arc::clone(&store); + let barrier = Arc::clone(&barrier); + std::thread::spawn(move || { + barrier.wait(); + store.register(&[target(index)]).is_ok() + }) + }) + .collect(); + let accepted = handles + .into_iter() + .map(|handle| handle.join().expect("should join")) + .filter(|accepted| *accepted) + .count(); + assert_eq!(accepted, 2); + assert_eq!(store.counts().expect("should count").inventory, 2); +} + +#[test] +fn inventory_cursor_and_consecutive_observations_survive_restart_without_reader_hints() { + let dir = tempfile::tempdir().expect("should create store"); + { + let store = Store::open(&dir.path().join("preparation.redb"), Bounds::default()) + .expect("should open"); + store + .register(&(0..6).map(target).collect::>()) + .expect("should register independent inventory"); + assert_eq!( + store.due_inventory(1000, 900, 2).expect("should scan"), + vec![target(0), target(1)] + ); + assert_eq!( + store + .observe_snapshot(&target(0), &[item('a').identity], 1000) + .expect("should observe"), + Some(vec![]) + ); + } + let store = Store::open(&dir.path().join("preparation.redb"), Bounds::default()) + .expect("should reopen"); + assert_eq!( + store + .due_inventory(1060, 900, 2) + .expect("should resume cursor"), + vec![target(2), target(3)] + ); + assert_eq!( + store + .observe_snapshot(&target(0), &[item('a').identity, item('b').identity], 1900) + .expect("should observe independently"), + Some(vec![item('a').identity]) + ); +} + +#[test] +fn retained_inventory_refuses_reduced_capacity_without_losing_targets_or_artifacts() { + let dir = tempfile::tempdir().expect("should create store"); + let path = dir.path().join("preparation.redb"); + let bounds = Bounds { + max_inventory: 15, + ..Bounds::default() + }; + let inventory: Vec<_> = (0..15).map(target).collect(); + let identity = item('a').identity; + let artifact = Artifact { + envelope: ResultEnvelope { + protocol: PREPARATION_PROTOCOL.to_owned(), + deployment_id: "retained-inventory-test".to_owned(), + identity: identity.clone(), + retirement_epoch: 0, + variant: String::new(), + item: EnvelopeItem::Text { + request: TextSignRequest { + text: "durable exact text".to_owned(), + page_url: "https://www.example.com/article/0000".to_owned(), + document_title: None, + boundary_selector: "article".to_owned(), + expected_policy_digest: "b".repeat(64), + cache_scope: TextCacheScope::ExactRequest, + }, + idempotency_key: "retained-operation".to_owned(), + }, + }, + target: inventory[0].clone(), + acknowledged: true, + }; + let job = { + let store = Store::open(&path, bounds).expect("should open at exact capacity"); + assert_eq!( + store + .register(&inventory) + .expect("should register all targets"), + 15 + ); + store + .observe_snapshot(&inventory[0], std::slice::from_ref(&identity), 1000) + .expect("should retain consecutive-observation evidence"); + assert_eq!( + store + .due_inventory(1000, 900, 1) + .expect("should advance cursor"), + vec![inventory[0].clone()] + ); + assert!( + store + .record_artifact(&artifact) + .expect("should retain real artifact bytes and metadata") + ); + store + .admit(&inventory[3], JobOrigin::Warm, &[], 1000) + .expect("should retain pending work"); + store + .job(&target_key(&inventory[3])) + .expect("should read pending work") + .expect("should retain job") + }; + let artifact_path = dir + .path() + .join("artifacts") + .join(format!("{identity}.json")); + let artifact_bytes = + std::fs::read(&artifact_path).expect("should read retained artifact before refusal"); + assert!(matches!( + Store::open( + &path, + Bounds { + max_inventory: 14, + ..bounds + } + ), + Err(StoreError::InventoryFull) + )); + assert_eq!( + std::fs::read(&artifact_path).expect("refused startup must preserve artifact bytes"), + artifact_bytes + ); + let store = Store::open(&path, bounds) + .expect("raising capacity back to the persisted count should release the refusal"); + store + .recover_storage() + .expect("normal startup recovery must retain valid stored work"); + assert_eq!(store.counts().expect("should count").inventory, 15); + assert_eq!( + store + .artifact(&identity) + .expect("should read durable artifact"), + Some(artifact) + ); + assert_eq!( + std::fs::read(&artifact_path).expect("adequate reopen must preserve exact artifact bytes"), + artifact_bytes + ); + assert_eq!( + store + .job(&target_key(&inventory[3])) + .expect("should read pending work after reopen"), + Some(job) + ); + assert_eq!( + store + .due_inventory(1001, 900, 1) + .expect("should resume the retained cursor"), + vec![inventory[1].clone()] + ); + assert_eq!( + store + .observe_snapshot(&inventory[0], std::slice::from_ref(&identity), 1001) + .expect("should read retained observation"), + Some(vec![identity]) + ); + let mut retained = store + .due_inventory(2000, 900, 128) + .expect("should enumerate every retained target without waiting"); + retained.sort_by(|left, right| left.path.cmp(&right.path)); + assert_eq!(retained, inventory); + assert_eq!( + store.register(&[target(15)]), + Err(StoreError::InventoryFull), + "reopening at the exact retained count must not create extra capacity" + ); +} + +#[test] +fn repeated_crashes_do_not_reset_attempts_or_the_one_hour_exhaustion_cooldown() { + let dir = tempfile::tempdir().expect("should create store"); + let bounds = Bounds { + max_attempts: 2, + ..Bounds::default() + }; + for token in 1..=2 { + let store = Store::open(&dir.path().join("preparation.redb"), bounds).expect("should open"); + if token == 1 { + store + .admit(&target(1), JobOrigin::Warm, &[], 10) + .expect("should admit"); + } + store + .recover_all(10) + .expect("should recover startup ownership"); + let claim = store + .claim(10, token) + .expect("should claim") + .expect("should retain unfinished work"); + assert_eq!(claim.job.attempts, token as u32); + } + let store = Store::open(&dir.path().join("preparation.redb"), bounds).expect("should reopen"); + store + .recover_all(11) + .expect("should recover exhausted claim"); + assert!( + store + .claim(11, 3) + .expect("should refuse third attempt") + .is_none() + ); + assert_eq!( + store + .admit(&target(1), JobOrigin::Reconcile, &[], 12) + .expect("should respect target cooldown"), + Admission::Damped + ); + assert_eq!( + store + .outcome(&target(1), "*") + .expect("should retain terminal damping") + .expect("should damp") + .class, + OutcomeClass::Exhausted + ); + assert!(matches!( + store + .admit(&target(1), JobOrigin::Reconcile, &[], 3612) + .expect("should allow after cooldown"), + Admission::Queued { .. } + )); +} + +#[test] +fn lease_boundary_and_restart_preserve_new_desired_generation_against_every_stale_settlement() { + let dir = tempfile::tempdir().expect("should create store"); + let bounds = Bounds { + lease_seconds: 10, + max_attempts: 1, + ..Bounds::default() + }; + let page = target(1); + let old = { + let store = Store::open(&dir.path().join("preparation.redb"), bounds).expect("should open"); + store + .admit(&page, JobOrigin::Signal, &[item('a')], 100) + .expect("should admit"); + let old = store + .claim(100, 1) + .expect("should claim") + .expect("should have work"); + assert!( + !store + .renew(&old.job.key, 2, 109) + .expect("should refuse another owner") + ); + assert!( + store + .renew(&old.job.key, old.token, 109) + .expect("should renew just before expiry") + ); + store + .admit(&page, JobOrigin::Signal, &[item('b')], 118) + .expect("should replace desired version during renewed lease"); + let desired = store + .job(&old.job.key) + .expect("should read") + .expect("should retain follow-up"); + assert_eq!(desired.generation, old.job.generation + 1); + assert_eq!( + store + .recover_expired(118) + .expect("should leave live owner alone"), + 0 + ); + assert!( + !store + .renew(&old.job.key, old.token, 119) + .expect("should refuse renewal at exact expiry") + ); + assert!( + !store + .complete(&old.job.key, old.token, 119) + .expect("should refuse success at exact expiry") + ); + assert!( + !store + .retry(&old.job.key, old.token, 119, "late_failure") + .expect("should refuse failure at exact expiry") + ); + assert_eq!( + store + .job(&old.job.key) + .expect("should preserve desired state"), + Some(desired) + ); + old + }; + let store = Store::open(&dir.path().join("preparation.redb"), bounds) + .expect("should reopen real owner"); + assert_eq!( + store + .recover_expired(119) + .expect("should recover exactly expired lease"), + 1 + ); + let pending = store + .job(&old.job.key) + .expect("should read") + .expect("should retain latest despite exhausted old attempt"); + assert_eq!(pending.state, JobState::Pending { ready_at: 119 }); + assert_eq!(pending.observed, vec![item('b')]); + let latest = store + .claim(119, 2) + .expect("should claim immediately") + .expect("should retain follow-up after restart"); + assert_eq!(latest.job.generation, old.job.generation + 1); + assert_eq!( + latest.job.attempts, 1, + "the old exhausted generation cannot spend the new generation's attempts" + ); + store + .record_claim_outcome( + &old, + &item('b').identity, + "stale_terminal", + OutcomeClass::Terminal, + 120, + ) + .expect("should fence old negative outcome"); + assert!( + !store + .renew(&old.job.key, old.token, 120) + .expect("should refuse old renewal") + ); + assert!( + !store + .complete(&old.job.key, old.token, 120) + .expect("should refuse old completion") + ); + assert!( + !store + .retry(&old.job.key, old.token, 120, "stale_retry") + .expect("should refuse old failure") + ); + assert_eq!( + store + .job(&latest.job.key) + .expect("should read current ownership"), + Some(latest.job.clone()) + ); + assert!( + store + .outcome(&page, &item('b').identity) + .expect("should read current outcome") + .is_none() + ); + assert!( + store + .complete(&latest.job.key, latest.token, 120) + .expect("should complete latest version") + ); + assert!( + store + .claim(120, 3) + .expect("should have no duplicate work") + .is_none() + ); +} + +#[test] +fn relevant_context_changes_clear_terminal_damping_but_never_retirement_or_retry_cooldown() { + let dir = tempfile::tempdir().expect("should create store"); + let store = + Store::open(&dir.path().join("preparation.redb"), Bounds::default()).expect("should open"); + store + .refresh_context("probe", "release-a") + .expect("should record authenticated release"); + store + .record_outcome( + &target(1), + &item('a').identity, + "unsupported_carrier", + OutcomeClass::Terminal, + 0, + ) + .expect("should damp terminal outcome"); + store + .record_outcome( + &target(1), + &item('b').identity, + "offline", + OutcomeClass::Exhausted, + 0, + ) + .expect("should damp exhausted attempt"); + store + .retire(&item('c').identity, "deployment") + .expect("should retire independently"); + store + .refresh_context("probe", "release-b") + .expect("should invalidate old terminal context"); + assert!( + store + .outcome(&target(1), &item('a').identity) + .expect("should read") + .is_none() + ); + assert!( + store + .outcome(&target(1), &item('b').identity) + .expect("should retain cooldown") + .is_some() + ); + assert!( + store + .retirement(&item('c').identity) + .expect("should retain retirement") + .is_some() + ); + assert_eq!( + store + .admit(&target(1), JobOrigin::Signal, &[item('c')], 1) + .expect("should refuse spent identity"), + Admission::Damped + ); +} + +#[test] +fn a_failed_marker_cannot_starve_later_retirements_and_cursor_survives_restart() { + let dir = tempfile::tempdir().expect("should create store"); + { + let store = Store::open(&dir.path().join("preparation.redb"), Bounds::default()) + .expect("should open"); + for index in 0..20 { + store + .retire(&format!("{index:064x}"), "deployment") + .expect("should queue marker"); + } + let first = store + .outbox_batch(16) + .expect("should publish bounded batch"); + assert_eq!(first.len(), 16); + assert_eq!(first[0].identity, format!("{:064x}", 0)); + // All sixteen sends fail: no acknowledgement, but the fair cursor is durable. + } + let store = Store::open(&dir.path().join("preparation.redb"), Bounds::default()) + .expect("should reopen"); + let next = store.outbox_batch(16).expect("should resume cursor"); + assert_eq!(next[0].identity, format!("{:064x}", 16)); + assert!( + next.iter() + .any(|marker| marker.identity == format!("{:064x}", 19)) + ); + assert_eq!( + store + .counts() + .expect("should count pending versus acknowledged") + .retirement_pending, + 20 + ); +} + +#[test] +fn operator_retry_fences_late_negative_outcomes_and_schedules_one_fresh_generation() { + let dir = tempfile::tempdir().expect("should create store"); + let store = + Store::open(&dir.path().join("preparation.redb"), Bounds::default()).expect("should open"); + let page = target(1); + store + .admit(&page, JobOrigin::Signal, &[item('a')], 1) + .expect("should admit"); + let old = store + .claim(1, 1) + .expect("should claim") + .expect("should have work"); + store + .clear_outcomes(&page, 2) + .expect("should authorize explicit operator retry"); + store + .record_claim_outcome( + &old, + &item('a').identity, + "late_integrity_failure", + OutcomeClass::Terminal, + 3, + ) + .expect("should fence old failure"); + assert!( + store + .outcome(&page, &item('a').identity) + .expect("should read") + .is_none() + ); + store + .complete(&old.job.key, old.token, 4) + .expect("should settle old claim"); + let fresh = store + .claim(4, 2) + .expect("should claim retry") + .expect("should preserve retry"); + assert!(fresh.job.generation > old.job.generation); + store + .record_claim_outcome( + &fresh, + &item('a').identity, + "current_failure", + OutcomeClass::Terminal, + 5, + ) + .expect("should record live failure"); + assert_eq!( + store + .outcome(&page, &item('a').identity) + .expect("should read") + .expect("should damp live outcome") + .code, + "current_failure" + ); +} + +#[test] +fn stale_snapshot_completion_cannot_replace_follow_up_stability_evidence() { + let dir = tempfile::tempdir().expect("should create store"); + let store = + Store::open(&dir.path().join("preparation.redb"), Bounds::default()).expect("should open"); + let page = target(1); + store + .register(std::slice::from_ref(&page)) + .expect("should register independent target"); + store + .admit(&page, JobOrigin::Signal, &[item('a')], 1) + .expect("should admit"); + let old = store + .claim(1, 1) + .expect("should claim") + .expect("should have work"); + assert_eq!( + store + .observe_claim_snapshot(&old, &[item('a').identity], 2) + .expect("should observe live claim"), + Some(Vec::new()) + ); + store + .admit(&page, JobOrigin::Signal, &[item('b')], 3) + .expect("should retain desired follow-up"); + assert_eq!( + store + .observe_claim_snapshot(&old, &[item('c').identity], 4) + .expect("should fence out-of-order snapshot"), + None + ); + store + .complete(&old.job.key, old.token, 5) + .expect("should schedule follow-up"); + let next = store + .claim(5, 2) + .expect("should claim") + .expect("should have follow-up"); + assert_eq!( + store + .observe_claim_snapshot(&next, &[item('b').identity], 6) + .expect("should observe current generation"), + Some(vec![item('a').identity]) + ); +} + +#[test] +fn reader_follow_up_cannot_consume_publisher_reservation_through_coalescing() { + let dir = tempfile::tempdir().expect("should create store"); + let store = Store::open( + &dir.path().join("preparation.redb"), + Bounds { + max_jobs: 2, + reserved_jobs: 1, + ..Bounds::default() + }, + ) + .expect("should open"); + let publication = target(0); + store + .admit(&publication, JobOrigin::Warm, &[], 0) + .expect("should admit publisher job"); + let publisher = store + .claim(0, 1) + .expect("should claim") + .expect("should process publisher job"); + store + .admit(&target(1), JobOrigin::Signal, &[item('a')], 1) + .expect("should use the reader slot"); + assert_eq!( + store + .admit(&publication, JobOrigin::Signal, &[item('b')], 2) + .expect("should account desired reader generation"), + Admission::QueueFull + ); + let retained = store + .job(&publisher.job.key) + .expect("should read") + .expect("should keep publisher work"); + assert_eq!(retained.origin, JobOrigin::Warm); + assert!( + retained.reserved && !retained.follow_up, + "refusal preserves publisher capacity and its in-flight claim" + ); +} diff --git a/crates/trusted-server-preparation-worker/tests/worker_flow.rs b/crates/trusted-server-preparation-worker/tests/worker_flow.rs new file mode 100644 index 0000000000..6a33b1461a --- /dev/null +++ b/crates/trusted-server-preparation-worker/tests/worker_flow.rs @@ -0,0 +1,900 @@ +//! Worker behavior over real redb and local HTTP boundaries. The API fixture +//! persists exact text receipts; it never stands in for the worker or probe. +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use axum::Router; +use axum::body::to_bytes; +use axum::extract::{Request, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use serde_json::{Value, json}; +use trusted_server_preparation_worker::client::{Api, Edge, Secrets, WorkerConfig}; +use trusted_server_preparation_worker::prepare::{Context, now, run}; +use trusted_server_preparation_worker::store::{ + Admission, Artifact, Bounds, Claim, JobOrigin, OutcomeClass, Store, target_key, +}; +use trusted_server_provenance_encypher::preparation::pipeline::FailureClass; +use trusted_server_provenance_encypher::preparation::protocol::{ + ItemKind, ObservedItem, PREPARATION_PROTOCOL, ResultEnvelope, SNAPSHOT_META_HEADER, + SnapshotMeta, SnapshotMode, SnapshotOrigin, SnapshotRequest, Target, +}; + +#[derive(Default)] +struct Boundary { + documents: HashMap, + receipts: HashMap, + signs: usize, + sign_requests: usize, + recovers: usize, + capabilities: usize, + publications: Vec, + creation_closed: bool, + lose_sign_reply: bool, + reject_publication: bool, + private: bool, + ranges: Vec<(u64, u64)>, + wrong_range_length: bool, +} + +struct Fixture { + state: Arc>, + url: String, + server: tokio::task::JoinHandle<()>, +} + +impl Fixture { + async fn start() -> Self { + let state = Arc::new(Mutex::new(Boundary::default())); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("should bind boundary"); + let url = format!( + "http://{}", + listener.local_addr().expect("should read address") + ); + let app = Router::new() + .fallback(boundary) + .with_state(Arc::clone(&state)); + let server = tokio::spawn(async move { + axum::serve(listener, app) + .await + .expect("should serve boundary"); + }); + Self { state, url, server } + } + + fn document(&self, path: &str, text: &str) { + self.state.lock().expect("should lock boundary").documents.insert(path.to_owned(), format!("Harbor
{text}
")); + } + + fn context(&self, dir: &tempfile::TempDir) -> Context { + let config: WorkerConfig = toml::from_str(&format!( + r#" +listen = "0.0.0.0:8112" +store_dir = {:?} +deployment_id = "fixture" +api_base_url = {:?} +domain = "example.com" +public_host = "www.example.com" +edge_url = {:?} +min_text_length = 1 +article_selector = "article" +max_large_media_bytes = 1048576 +max_snapshot_bytes = 1048576 +max_media_bytes = 1048576 +[secrets] +worker_token = "FIXTURE_WORKER" +signal_secret = "FIXTURE_SIGNAL" +publication_secret = "FIXTURE_PUBLICATION" +operator_secret = "FIXTURE_OPERATOR" +"#, + dir.path().to_str().expect("should have path"), + self.url, + self.url + )) + .expect("should parse worker configuration"); + let store = Arc::new( + Store::open(&dir.path().join("preparation.redb"), Bounds::default()) + .expect("should open real store"), + ); + store.recover_storage().expect("should recover real files"); + store.recover_all(now()).expect("should recover claims"); + let secrets = Secrets { + worker_token: "cwk_fixture".to_owned(), + signal: "signal".to_owned(), + publication: "publication".to_owned(), + operator: "operator".to_owned(), + canary: None, + }; + Context::new( + store, + Api::new(&self.url, &secrets), + Edge::new(&self.url, &secrets, "fixture"), + Arc::new(config), + &secrets.worker_token, + ) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + self.server.abort(); + } +} + +fn response(status: StatusCode, value: Value) -> Response { + (status, axum::Json(value)).into_response() +} + +async fn boundary(State(state): State>>, request: Request) -> Response { + let path = request.uri().path().to_owned(); + let headers = request.headers().clone(); + let bytes = to_bytes(request.into_body(), 8 * 1024 * 1024) + .await + .expect("should read bounded request"); + let value: Value = serde_json::from_slice(&bytes).unwrap_or(Value::Null); + let mut state = state.lock().expect("should lock boundary"); + if path.ends_with("/policy") { + return response(StatusCode::OK, json!({"policy_digest": "a".repeat(64)})); + } + if path.ends_with("/capabilities") { + state.capabilities += 1; + return response( + StatusCode::FORBIDDEN, + json!({"error":{"code":"CREATION_DISABLED"}}), + ); + } + if path == "/api/v1/public/cdn/sign" || path == "/api/v1/public/cdn/sign/recover" { + assert_eq!( + value["cache_scope"], "exact_request", + "both signing and recovery must use the exact-request API cache partition" + ); + assert_eq!( + headers + .get("x-encypher-worker-token") + .expect("should authenticate API"), + "cwk_fixture" + ); + let key = headers + .get("idempotency-key") + .expect("should carry operation identity") + .to_str() + .expect("should be text") + .to_owned(); + if path.ends_with("/recover") { + state.recovers += 1; + return match state.receipts.get(&key) { + Some((frozen, answer)) if frozen == &value => { + response(StatusCode::OK, answer.clone()) + } + Some(_) => response(StatusCode::CONFLICT, json!({"error":{"code":"CONFLICT"}})), + None => response(StatusCode::NOT_FOUND, json!({"error":{"code":"MISSING"}})), + }; + } + state.sign_requests += 1; + if state.creation_closed { + return response( + StatusCode::FORBIDDEN, + json!({"error":{"code":"CREATION_DISABLED"}}), + ); + } + assert!( + !state.receipts.contains_key(&key), + "the worker must recover a completed operation, not call sign as replay" + ); + state.signs += 1; + let answer = json!({"success":true,"policy_digest":"a".repeat(64),"embedding_plan":{"index_unit":"codepoint","operations":[{"insert_after_index":0,"marker":"\u{fe00}"}]},"manifest_url":"https://manifest.example.com/receipt"}); + state.receipts.insert(key, (value, answer.clone())); + if state.lose_sign_reply { + state.lose_sign_reply = false; + return response( + StatusCode::SERVICE_UNAVAILABLE, + json!({"error":{"code":"LOST_REPLY"}}), + ); + } + return response(StatusCode::OK, answer); + } + assert_eq!( + headers + .get("x-encypher-preparation-token") + .expect("should authenticate edge"), + "publication" + ); + if path == "/_encypher/preparation/snapshot" { + let request: SnapshotRequest = + serde_json::from_value(value).expect("should send snapshot request"); + let Some(document) = state.documents.get(&request.target.path) else { + return response( + StatusCode::FORBIDDEN, + json!({"error":{"code":"target_not_configured"}}), + ); + }; + let mut body = document.as_bytes().to_vec(); + let mut origin_status = 200; + let length = body.len() as u64; + match request.read { + SnapshotMode::Whole => {} + SnapshotMode::Head => body.clear(), + SnapshotMode::Range { first, last, .. } => { + body = body[first as usize..=last as usize].to_vec(); + if state.wrong_range_length { + body.pop(); + } + state.ranges.push((first, last)); + origin_status = 206; + } + } + let meta = SnapshotMeta { + protocol: PREPARATION_PROTOCOL.to_owned(), + page_url: format!("https://www.example.com{}", request.target.path), + content_type: "text/html".to_owned(), + etag: Some("\"stable\"".to_owned()), + length: Some(length), + origin: SnapshotOrigin { + status: origin_status, + private: state.private, + no_transform: false, + manifest_link: false, + content_encoding: String::new(), + origin_host: "origin.example.com".to_owned(), + origin_identity_sha256: String::new(), + source_reference: Some(format!( + "https://origin.example.com{}", + request.target.path + )), + }, + }; + let encoded = STANDARD.encode(serde_json::to_vec(&meta).expect("should encode metadata")); + return ( + [ + (SNAPSHOT_META_HEADER, encoded), + ("cache-control", "private, no-store".to_owned()), + ], + body, + ) + .into_response(); + } + if path.starts_with("/_encypher/preparation/results/") { + if state.reject_publication { + return response( + StatusCode::SERVICE_UNAVAILABLE, + json!({"error":{"code":"UNAVAILABLE"}}), + ); + } + state + .publications + .push(serde_json::from_value(value).expect("should publish a real envelope")); + return response(StatusCode::OK, json!({"stored":true})); + } + if path.starts_with("/_encypher/preparation/markers/") { + return response(StatusCode::OK, json!({"stored":true})); + } + response( + StatusCode::NOT_FOUND, + json!({"error":{"code":"unexpected_route"}}), + ) +} + +fn target(path: &str) -> Target { + Target { + path: path.to_owned(), + variant: String::new(), + } +} + +fn claim( + context: &Context, + target: &Target, + origin: JobOrigin, + items: &[ObservedItem], + token: u64, +) -> Claim { + assert!(matches!( + context + .store + .admit(target, origin, items, now()) + .expect("should admit"), + Admission::Queued { .. } | Admission::Coalesced { .. } + )); + context + .store + .claim(now(), token) + .expect("should claim") + .expect("should have work") +} + +async fn warm(context: &Context, target: &Target, token: u64) { + let claim = claim(context, target, JobOrigin::Warm, &[], token); + run(context, &claim).await.expect("should finish warm"); + assert!( + context + .store + .complete(&claim.job.key, claim.token, now()) + .expect("should settle") + ); +} + +#[tokio::test] +async fn changed_only_preparation_reuses_stable_siblings_and_restores_evicted_delivery_records() { + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + fixture.document("/a", "The harbor vote passed."); + fixture.document("/b", "The ferry leaves at noon."); + let context = fixture.context(&dir); + warm(&context, &target("/a"), 1).await; + warm(&context, &target("/b"), 2).await; + assert_eq!(fixture.state.lock().expect("should lock").signs, 2); + let original = fixture + .state + .lock() + .expect("should lock") + .publications + .clone(); + fixture.document("/a", "The harbor vote was amended."); + warm(&context, &target("/a"), 3).await; + warm(&context, &target("/b"), 4).await; + let state = fixture.state.lock().expect("should lock"); + assert_eq!(state.signs, 3, "only the changed exact identity signs"); + assert_ne!(state.publications[2].identity, original[0].identity); + assert_eq!( + state.publications[3], original[1], + "delivery eviction restores exactly the retained envelope" + ); + assert_eq!(context.store.counts().expect("should count").artifacts, 3); +} + +#[tokio::test] +async fn title_and_mirror_url_changes_take_new_exact_operations_but_unchanged_text_reuses() { + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + fixture.document("/a", "The harbor vote passed."); + let context = fixture.context(&dir); + warm(&context, &target("/a"), 1).await; + { + let mut state = fixture.state.lock().expect("should lock boundary"); + let original = state.documents["/a"].clone(); + state.documents.insert( + "/a".to_owned(), + original.replace( + "Harbor", + "Revised harbor title", + ), + ); + } + warm(&context, &target("/a"), 2).await; + { + let mut state = fixture.state.lock().expect("should lock boundary"); + let revised = state.documents["/a"].clone(); + state.documents.insert("/mirror".to_owned(), revised); + } + warm(&context, &target("/mirror"), 3).await; + warm(&context, &target("/mirror"), 4).await; + let state = fixture.state.lock().expect("should lock boundary"); + assert_eq!(state.signs, 3, "only new exact text requests should sign"); + assert_eq!( + state.receipts.len(), + 3, + "each metadata context has its own operation" + ); + assert_ne!( + state.publications[0].identity, + state.publications[1].identity + ); + assert_ne!( + state.publications[1].identity, + state.publications[2].identity + ); + assert_eq!(state.publications[2], state.publications[3]); + let requests: Vec<&Value> = state + .receipts + .values() + .map(|(request, _)| request) + .collect(); + assert!( + requests + .iter() + .all(|request| request["text"] == "The harbor vote passed.") + ); + assert!( + requests + .iter() + .any(|request| request["document_title"] == "Harbor") + ); + assert!(requests.iter().any(|request| { + request["document_title"] == "Revised harbor title" + && request["page_url"] == "https://www.example.com/a" + })); + assert!( + requests + .iter() + .any(|request| request["page_url"] == "https://www.example.com/mirror") + ); +} + +#[tokio::test] +async fn six_hour_store_maintenance_and_restart_keep_exact_artifact_reusable_with_creation_closed() +{ + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + let page = target("/a"); + fixture.document( + "/a", + "Unchanged editorial content needs no hourly signing renewal.", + ); + let context = fixture.context(&dir); + warm(&context, &page, 1).await; + let original = fixture.state.lock().expect("should lock").publications[0].clone(); + let retained = context + .store + .artifact(&original.identity) + .expect("should read") + .expect("should retain durable artifact"); + let charge = context + .store + .reserved() + .expect("should charge artifact and reconstruction"); + let maintenance_start = now(); + fixture.state.lock().expect("should lock").creation_closed = true; + // Store maintenance accepts explicit Unix seconds. This advances those + // controls only; preparation's SystemTime clock does not advance. + for hours in [1, 3, 6] { + let at = maintenance_start + hours * 3600; + assert_eq!( + context + .store + .recover_expired(at) + .expect("should sweep expired claims"), + 0 + ); + assert_eq!( + context + .store + .prune_expired_outcomes(at) + .expect("should sweep expired outcomes"), + 0 + ); + assert_eq!( + context + .store + .artifact(&original.identity) + .expect("should read retained result"), + Some(retained.clone()), + "multi-hour maintenance must not time-expire an unchanged artifact" + ); + assert_eq!( + context + .store + .reserved() + .expect("should preserve quota charge"), + charge + ); + } + drop(context); + let context = fixture.context(&dir); + assert_eq!( + context + .store + .artifact(&original.identity) + .expect("should read after recovery"), + Some(retained) + ); + assert_eq!( + context + .store + .reserved() + .expect("should retain charged reconstruction after restart"), + charge + ); + let recovers_before = fixture.state.lock().expect("should lock").recovers; + for token in 2..=4 { + warm(&context, &page, token).await; + } + let state = fixture.state.lock().expect("should lock"); + assert_eq!(state.signs, 1); + assert_eq!( + state.sign_requests, 1, + "unchanged reuse must not even attempt a denied signing request" + ); + assert_eq!( + state.recovers, recovers_before, + "a retained artifact needs no worker-side API replay or renewal" + ); + assert_eq!(state.capabilities, 0); + assert_eq!(state.publications.len(), 4); + assert!( + state + .publications + .iter() + .all(|envelope| envelope == &original), + "delivery eviction is repaired by the same immutable artifact and operation" + ); + assert_eq!( + context + .store + .counts() + .expect("should retain one version") + .artifacts, + 1 + ); +} + +#[tokio::test] +async fn crash_after_api_commit_recovers_with_creation_closed_and_never_calls_sign_again() { + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + fixture.document("/a", "A durable exact text receipt."); + { + let context = fixture.context(&dir); + fixture.state.lock().expect("should lock").lose_sign_reply = true; + let claim = claim(&context, &target("/a"), JobOrigin::Warm, &[], 1); + assert_eq!( + run(&context, &claim) + .await + .expect_err("should lose API reply") + .class, + FailureClass::Transient + ); + assert_eq!(fixture.state.lock().expect("should lock").signs, 1); + // Simulate process death without settling: redb and attempted operation survive. + } + fixture.state.lock().expect("should lock").creation_closed = true; + let context = fixture.context(&dir); + let claim = context + .store + .claim(now(), 2) + .expect("should reclaim") + .expect("should recover unfinished job"); + run(&context, &claim) + .await + .expect("should recover with signing disabled"); + let state = fixture.state.lock().expect("should lock"); + assert_eq!(state.signs, 1); + assert_eq!(state.publications.len(), 1); + assert!(state.recovers >= 2); + assert_eq!( + state.capabilities, 0, + "text recovery never depends on creation capability" + ); +} + +#[tokio::test] +async fn durable_artifact_survives_lost_publication_and_restart_without_api_creation() { + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + fixture.document("/a", "Retained output survives restart."); + { + let context = fixture.context(&dir); + fixture + .state + .lock() + .expect("should lock") + .reject_publication = true; + let claim = claim(&context, &target("/a"), JobOrigin::Warm, &[], 1); + run(&context, &claim) + .await + .expect_err("should retain before failed push"); + assert_eq!(context.store.counts().expect("should count").artifacts, 1); + } + { + let mut state = fixture.state.lock().expect("should lock"); + state.reject_publication = false; + state.creation_closed = true; + } + let context = fixture.context(&dir); + let claim = context + .store + .claim(now(), 2) + .expect("should reclaim") + .expect("should have work"); + run(&context, &claim) + .await + .expect("should republish durable artifact"); + let state = fixture.state.lock().expect("should lock"); + assert_eq!(state.signs, 1); + assert_eq!(state.publications.len(), 1); + assert!( + context + .store + .artifact(&state.publications[0].identity) + .expect("should read") + .expect("should retain") + .acknowledged + ); +} + +#[tokio::test] +async fn private_snapshot_refuses_before_policy_or_signing_and_is_damped_per_observation() { + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + fixture.document("/a", "Private source must never be admitted."); + fixture.state.lock().expect("should lock").private = true; + let context = fixture.context(&dir); + let observed = ObservedItem { + identity: "b".repeat(64), + kind: ItemKind::Text, + }; + let claim = claim( + &context, + &target("/a"), + JobOrigin::Signal, + std::slice::from_ref(&observed), + 1, + ); + run(&context, &claim) + .await + .expect("should record privacy refusal"); + let outcome = context + .store + .outcome(&target("/a"), &observed.identity) + .expect("should read") + .expect("should damp"); + assert_eq!(outcome.class, OutcomeClass::Terminal); + assert_eq!(outcome.code, "source_private"); + assert_eq!(fixture.state.lock().expect("should lock").signs, 0); +} + +#[tokio::test] +async fn unstable_child_does_not_suppress_stable_text_or_create_signing_churn() { + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + fixture.document("/a", "Stable editorial text alongside a rotating child."); + let context = fixture.context(&dir); + warm(&context, &target("/a"), 1).await; + let identity = fixture.state.lock().expect("should lock").publications[0] + .identity + .clone(); + let rotating = ObservedItem { + identity: "c".repeat(64), + kind: ItemKind::Embedded, + }; + let stable = ObservedItem { + identity: identity.clone(), + kind: ItemKind::Text, + }; + let claim = claim( + &context, + &target("/a"), + JobOrigin::Signal, + &[rotating.clone(), stable], + 2, + ); + run(&context, &claim) + .await + .expect("should reuse stable sibling"); + assert_eq!( + context + .store + .outcome(&target("/a"), &rotating.identity) + .expect("should read") + .expect("should damp") + .class, + OutcomeClass::SourceUnstable + ); + let state = fixture.state.lock().expect("should lock"); + assert_eq!(state.signs, 1); + assert_eq!(state.publications.len(), 2); + assert_eq!(state.publications[1].identity, identity); +} + +#[tokio::test] +async fn independent_inventory_requires_consecutive_stability_and_survives_restart() { + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + fixture.document( + "/registered", + "An unseen page with independent publisher inventory.", + ); + { + let context = fixture.context(&dir); + context + .store + .register(&[target("/registered")]) + .expect("should register without reader hint"); + let due = context + .store + .due_inventory(1000, 900, 128) + .expect("should scan"); + assert_eq!(due, vec![target("/registered")]); + let claim = claim(&context, &due[0], JobOrigin::Reconcile, &[], 1); + run(&context, &claim).await.expect("should observe only"); + context + .store + .complete(&claim.job.key, claim.token, now()) + .expect("should settle"); + assert_eq!(fixture.state.lock().expect("should lock").signs, 0); + } + let context = fixture.context(&dir); + let claim = claim( + &context, + &target("/registered"), + JobOrigin::Reconcile, + &[], + 2, + ); + run(&context, &claim) + .await + .expect("should prepare consecutive stable snapshot"); + assert_eq!(fixture.state.lock().expect("should lock").signs, 1); +} + +#[tokio::test] +async fn retired_result_cannot_be_recovered_by_warm_or_late_claim_across_restart() { + let fixture = Fixture::start().await; + let dir = tempfile::tempdir().expect("should create store"); + let page = target("/a"); + fixture.document("/a", "An explicitly retired identity stays spent."); + let (artifact, late, epoch) = { + let context = fixture.context(&dir); + warm(&context, &page, 1).await; + let envelope = fixture.state.lock().expect("should lock").publications[0].clone(); + let artifact = Artifact { + envelope, + target: page.clone(), + acknowledged: true, + }; + let late = claim(&context, &page, JobOrigin::Warm, &[], 2); + let epoch = context + .store + .retire(&artifact.envelope.identity, "fixture") + .expect("should retire"); + let counts = context + .store + .counts() + .expect("should report local retirement separately"); + assert_eq!(counts.artifacts, 0); + assert_eq!(counts.retirement_pending, 1); + assert_eq!(counts.retirement_acknowledged, 0); + assert_eq!( + context + .store + .outbox() + .expect("should persist marker before delivery") + .len(), + 1 + ); + (artifact, late, epoch) + }; + fixture.state.lock().expect("should lock").creation_closed = true; + { + let context = fixture.context(&dir); + let identity = &artifact.envelope.identity; + let marker = context + .store + .outbox() + .expect("should retain delayed marker across restart") + .pop() + .expect("should have marker"); + assert_eq!(marker.identity, *identity); + assert_eq!(marker.epoch, epoch); + assert_eq!( + context + .store + .retire(identity, "fixture") + .expect("should preserve existing retirement"), + epoch + ); + assert!( + !context + .store + .record_artifact(&artifact) + .expect("should reject delayed old artifact") + ); + assert!( + context + .store + .artifact(identity) + .expect("should refuse retired result") + .is_none() + ); + assert_eq!( + run(&context, &late) + .await + .expect_err("should fence pre-restart worker") + .code, + "lease_lost" + ); + let recovered = context + .store + .claim(now(), 3) + .expect("should claim unfinished warm") + .expect("should retain unfinished target"); + run(&context, &recovered) + .await + .expect("should refuse retired identity without signing"); + assert!( + context + .store + .complete(&recovered.job.key, recovered.token, now()) + .expect("should settle retired work") + ); + warm(&context, &page, 4).await; + let observed = ObservedItem { + identity: identity.clone(), + kind: ItemKind::Text, + }; + assert_eq!( + context + .store + .admit(&page, JobOrigin::Signal, &[observed], now()) + .expect("should refuse reader resurrection"), + Admission::Damped + ); + context + .store + .acknowledge_marker(identity, epoch + 1) + .expect("should ignore wrong acknowledgement"); + assert_eq!( + context + .store + .outbox() + .expect("should keep pending exact epoch"), + vec![marker] + ); + assert_eq!( + context + .store + .counts() + .expect("should report still pending") + .retirement_pending, + 1 + ); + context + .store + .acknowledge_marker(identity, epoch) + .expect("should acknowledge exact marker"); + assert!( + context + .store + .outbox() + .expect("should settle outbox") + .is_empty() + ); + let counts = context + .store + .counts() + .expect("should distinguish acknowledged retirement"); + assert_eq!(counts.retirement_pending, 0); + assert_eq!(counts.retirement_acknowledged, 1); + } + let context = fixture.context(&dir); + assert!( + context + .store + .retirement(&artifact.envelope.identity) + .expect("should read tombstone after second restart") + .expect("should remain retired") + .acknowledged + ); + assert!( + context + .store + .outbox() + .expect("should retain acknowledged state") + .is_empty() + ); + assert!( + !context + .store + .record_artifact(&artifact) + .expect("should reject late result even after marker acknowledgement") + ); + warm(&context, &page, 5).await; + assert!( + context + .store + .job(&target_key(&page)) + .expect("should read") + .is_none() + ); + let state = fixture.state.lock().expect("should lock"); + assert_eq!(state.signs, 1); + assert_eq!(state.sign_requests, 1); + assert_eq!( + state.publications.len(), + 1, + "no late claim, warm or restart can publish retired bytes again" + ); +} diff --git a/crates/trusted-server-preparation-worker/worker.example.toml b/crates/trusted-server-preparation-worker/worker.example.toml new file mode 100644 index 0000000000..d62428bb7f --- /dev/null +++ b/crates/trusted-server-preparation-worker/worker.example.toml @@ -0,0 +1,43 @@ +# Launch: trusted-server-axum preparation-worker --config worker.toml +# Store ownership is exclusive. Operator commands use HTTP, not a second DB open. +listen = "0.0.0.0:8112" +store_dir = "/var/lib/trusted-server/preparation" +deployment_id = "publisher-preparation" +api_base_url = "https://api.example.com" +domain = "example.com" +public_host = "www.example.com" +edge_url = "https://delivery.example.com" + +# Match the delivery instance's projection, source ceilings and variants. +article_selector = "article" +min_text_length = 50 +max_media_bytes = 5000000 +max_snapshot_bytes = 67108864 +max_large_media_bytes = 1073741824 +max_preparation_seconds = 120 +variants = [] + +# Sources, temporary assembly, probes, plans, outputs and their identity, +# outcome and reference metadata share this hard quota. Full storage refuses +# new work; retained recovery material is never evicted. Conservative +# workspace is reserved before signing. +workers = 4 +artifact_quota_bytes = 10737418240 +scan_per_minute = 128 +scan_interval_seconds = 900 + +# Independent publisher inventory survives lost reader hints and process restarts. +# An unregistered page is opportunistic coverage only, not universal discovery. +[[inventory]] +path = "/news/harbor-vote" +variant = "" + +[secrets] +# Values come from the process environment, not this file. The signal and +# publication values must equal the corresponding delivery secret-store entries. +worker_token = "TS_PREPARATION_WORKER_TOKEN" +signal_secret = "TS_PREPARATION_SIGNAL_SECRET" +publication_secret = "TS_PREPARATION_PUBLICATION_SECRET" +operator_secret = "TS_PREPARATION_OPERATOR_SECRET" +# Optional, for an authorized staging creation canary only: +# canary_token = "TS_PREPARATION_CANARY_TOKEN" diff --git a/docs/guide/asset-routes.md b/docs/guide/asset-routes.md index 385c26ecc6..6e31d591ea 100644 --- a/docs/guide/asset-routes.md +++ b/docs/guide/asset-routes.md @@ -220,6 +220,88 @@ Set the configured debug parameter to `1` to disable Image Optimizer for one req The asset route still matches. Path rewrite, origin query policy, and S3 signing still run. The response comes from the unoptimized origin object. +## Optimized images and C2PA provenance + +Where a media signer is installed, such as the Encypher provenance module, an optimized request also asks Fastly Image Optimizer to keep its input's C2PA manifest (`metadata=c2pa`). Image Optimizer then writes a manifest of its own that records the transformations it performed and binds the input it read by hash. The answer is signed only when its body carries a C2PA manifest store, and the signature records no operation of Trusted Server's own: Image Optimizer's manifest is the parent of what is signed, and the signed answer carries `Vary: Accept`. + +Image Optimizer writes that manifest only when its input already carries one. An optimized answer of an image without C2PA provenance records nothing about what was done to it, so it is delivered as Image Optimizer made it, unsigned. Only a whole `200` answer to a `GET` is signed; ranges and `HEAD` requests on an optimized route are answered as Image Optimizer answers them. The SDK's metadata option offers only `copyright`, so the adapter sends the filters as the `metadata` query parameter, which Image Optimizer removes from its origin request, so an S3 signature over the origin address is unaffected. Where the request already asks for the `copyright` filter, the adapter sends `metadata=copyright,c2pa`, which applies both. + +Fastly enables the `c2pa` filter for every image format Image Optimizer supports, and notes that some C2PA verification tooling may not handle JPEG XL. Whether Trusted Server signs an optimized answer depends on the media signer accepting the format it reads from the answer's bytes. None of this has been observed on a deployed Image Optimizer service yet: Viceroy does not run Image Optimizer. + +Two ways give Image Optimizer a signed input: + +- The origin serves C2PA-signed originals. +- The route sets `signed_source = true`. Image Optimizer is then sent to this host, at the reader's path with the profile set's debug parameter set to `1` and no other query, instead of to the origin. The route answers that request without Image Optimizer, with the exact origin bytes it captured, signed with a manifest that opens them. Image Optimizer keeps that manifest and adds its own, so the chain reads: the signed original, Image Optimizer's transformations, and the signature over Image Optimizer's answer. Every optimized image costs this service a second request, and the request Image Optimizer sends reaches this host as a reader's would. Where Image Optimizer answers with a `4xx` or `5xx` or cannot be reached, for example while signing is refused and the original goes out `private`, which Image Optimizer will not transform, the route answers once from the origin without Image Optimizer, so an origin's own error still reaches the reader as the origin made it. + +An optimized answer whose origin headers mark it as one reader's (`private`, `no-store`, a cookie or a `Vary` on the reader) is never signed, and keeps the origin's own `Cache-Control` in place of the route's `[[cache.asset_rules]]` policy. + +The manifests embedded in an optimized image are history. A policy revocation stops the signature Trusted Server adds to each answer, but an image Image Optimizer already holds keeps the earlier manifests until its cache expires it, bounded by the origin's own `Cache-Control`. Revoking the signing certificate is what invalidates those manifests for verifiers. The debug parameter is public, so any reader can request the signed, unoptimized original the same way. On Axum, Cloudflare and Spin, whose HTTP clients refuse Image Optimizer metadata, a `signed_source` route therefore answers from the origin, signed and unoptimized, where a route without it answers `502`. + +## Rehosting third-party files + +A route that sets `rehost_page_references = true` serves a third party's public files from the publisher's own host, so a media signer can sign them and a stylesheet `[[serve]]` entry can run on them. Pages served through the publisher pipeline then name the route's address in place of the third party's. + +This covers eligible bytes that pass through the configured routes, not every +media item a browser may load. Direct third-party frame or script requests +and SVG created later by scripts remain outside these routes. See the +[provenance coverage limits](./configuration#placing-page-changes) before +claiming page-wide coverage. + +With Encypher's `execution = "async_prepared"`, a routed or rehosted file is +never signed during delivery. It carries provenance only after the preparation +worker prepared that exact path, as its own target or from a reader hint, and +the route prefix must be listed in the preparation `routes`. Preparing the page +that links the file does not prepare it. See +[Encypher prepared signing](./configuration#encypher-prepared-signing). + +```toml +[[proxy.asset_routes]] +prefix = "/_rehost/assets.example.com/library/" +rehost_page_references = true + +[proxy.asset_routes.source] +module = "origin" + +[proxy.asset_routes.source.origin] +url = "https://assets.example.com/library" + +[[serve]] +media_type = "text/css" +path = "/_rehost/assets.example.com/library/carousel/" +middleware = ["provenance.encypher.stylesheet"] +``` + +The route's request path below its prefix is appended to the origin's path, byte for byte, and the reader's query is sent as written. The prefix must end in `/` and must not sit under `/_ts`. Settings are refused at startup where the route also sets `path_pattern`, `target_path` or `image_optimizer`, where its source is not the `origin` source over `https`, signs its origin requests, overrides `Host` with an authority other than the URL's own (including a non-default port), sets a `directory_index` or strips the query, or where two rehosting routes overlap. + +### What is rewritten in a page + +An address is rewritten only where it names a file inside a rehosting route's origin folder, the route that request dispatch would pick for the rewritten path is that same route, and that route fetches exactly the address the page wrote. The rewritten address keeps the page's query and fragment, and carries any path prefix request addressing removed. + +When any rehosting route is enabled, the publisher buffers the complete decoded original HTML under `publisher.max_buffered_body_bytes` before rewriting. The first effective HTML `base[href]` resolves against the full fetched document URL, including its path and query. It applies even to references preceding that element; later bases, comments, raw text, template contents and foreign-namespace base elements do not replace it. Relative references resolve against that base, and protocol-relative references take its scheme, never an automatic HTTPS upgrade. An oversized or unreadable original is refused before any rewritten output is emitted. Deployments with no rehosting route keep the normal streaming path. + +HTML attribute references are decoded before URL resolution and serialized once afterward. Percent escapes, query ordering and fragments keep their decoded-attribute bytes. In both `srcset` and `imagesrcset`, the publisher-origin fallback changes only a leading URL authority or supported bare host with a host boundary. A publisher name inside another host, path, query or fragment stays unchanged. + +These references are read: + +- media: `img`, `source`, `video`, `audio` and `track` `src`, `img` and `source` `srcset` candidates, `link` `imagesrcset` candidates, `video` `poster`, `input type="image"` `src`, and `link rel="preload"` or `rel="prefetch"` with `as` set to `image`, `font`, `audio`, `video` or `track`; +- stylesheets: `link rel="stylesheet"` `href`, and preloads with `as="style"`. A stylesheet is rewritten only where a `[[serve]]` entry for `text/css` covers its route path, because a stylesheet served from the route unmoved would name its relative files at the wrong address. + +An element with an `integrity` attribute keeps its address, since the browser would refuse any body other than the one hashed. An address already on a rehosting route is left as it is. Any other address is left to the page's other rewriters. + +### What the route answers + +A rehosting route sends no `Referer` to the third party, and asks for `Accept-Encoding: identity`, so a body it moves or signs is never compressed. + +- A redirect is answered with its `Location` resolved against the upstream address and written as a rehosting route's address where one serves it, otherwise as the absolute upstream address. Nothing follows a redirect at the edge. Its discarded origin body contributes no encoding, length, digest, validator or range metadata to the empty response; cache and privacy headers remain applicable. +- A stylesheet whose route path a `[[serve]]` entry covers is moved: each `url()`, `src()`, `image-set()` candidate and `@import` it names is resolved against its upstream address and written as a rehosting route's address where one serves the file (an `@import` only where its own path is covered), or as the absolute upstream address. Fragment-only references and `data:` addresses stay as written. The entry's middleware then run on the moved stylesheet, and the answer carries no validator, `Content-Encoding` or range support of the origin's, since its bytes are this service's. +- A stylesheet that cannot be moved is answered with a `307` to its upstream address, held by no cache (`Cache-Control: private, no-store`). That covers a stylesheet no `[[serve]]` entry covers, a status other than `200`, a body still content-coded, a `private` or `no-transform` origin answer, a charset other than UTF-8 (in `Content-Type` or a leading `@charset`), a body over 10 MiB, or a refusal by the entry's middleware. +- A `HEAD` request for a moved stylesheet is answered with the origin's metadata only, without its length or validators. +- Anything else is media, answered as any route's is, and signed where a media signer accepts it. + +On a route path a stylesheet entry covers, the origin is asked without the reader's conditional or range headers, and `If-Match`, `If-None-Match`, `If-Modified-Since` and `If-Unmodified-Since` are answered against what is delivered, by its own `ETag` and `Last-Modified`. A moved stylesheet carries neither, so `If-Match` on it gives `412`. A range is not applied on those paths: the whole answer is sent. + +CSS-chain candidate paths bypass the route's raw-copy cache before the response MIME type is known, including images or fonts under the same prefix. A moved stylesheet keeps browser freshness (`max-age`) separate from shared freshness (`s-maxage`, falling back to `max-age`). Each subtracts the greater of received `Age` and apparent age from `Date`, plus elapsed processing time, then takes its corresponding browser or shared route cap. These bounds run after middleware headers and survive adapter finalization. Missing, malformed, expired or non-reusable origin freshness authorizes no fresh copy. Only after calculating the remaining lifetimes does rebasing remove the origin's `Age`, `Date` and `Expires`; stale allowances and `immutable` do not transfer to the new representation. Streamed stylesheet bodies use the existing bounded collector, which checks remaining capacity before appending a chunk. Buffered bodies reuse their owned bytes without copying. + ## Local testing notes Viceroy does not perform real Fastly Image Optimizer transformations. Local tests can verify routing, query stripping, SigV4 headers, and metadata attachment. End-to-end image transformation verification requires a deployed Fastly Compute service with Image Optimizer enabled. diff --git a/docs/guide/axum-dev.md b/docs/guide/axum-dev.md index 7cb79cb442..657ed3dbdd 100644 --- a/docs/guide/axum-dev.md +++ b/docs/guide/axum-dev.md @@ -66,6 +66,42 @@ its `ext.debug` echo of the resolved request) when the `[demand.]` table of a Prebid Server demand source sets `debug = true`. Enable it only for short local debugging sessions. +## Runtime KV + +Generic runtime KV (`RuntimeServices::kv_store`) is off by default. Set +`TRUSTED_SERVER_KV_PATH` to a file path before starting the server and the dev +server opens one persistent store at that path, creating missing directories, +and shares it across all requests. Data survives restarts; two servers pointed +at different paths do not see each other's data. If the variable is set but the +store cannot be opened, the server exits with an error. When it is unset, +runtime KV reports unavailable and a warning is logged once. + +Encypher's `execution = "async_prepared"` stores published results and +retirement markers in this store, so set `TRUSTED_SERVER_KV_PATH` for that +mode. + +## Listener address + +The bind address comes from `axum.toml`, or from `PORT`, which binds that port +on loopback. `TS_LISTEN_ADDR`, a full socket address such as `10.0.0.5:8080`, +overrides both, for example to reach the server from a private network. A +value that does not parse as a socket address exits with status `2`. + +## Preparation worker + +The same binary runs Encypher's publisher-operated preparation worker as a +separate long-lived process with its own store: + +```bash +trusted-server-axum preparation-worker --config worker.toml +``` + +Any other argument form prints the usage line and exits with status `2`; a +configuration, store or listener failure exits with status `1`. The worker +does not serve publisher traffic. Its configuration, secrets, readiness and +operator commands are described under +[Encypher prepared signing](./configuration#encypher-prepared-signing). + ## Runtime boundaries Axum sanitizes forwarded client-IP headers but does not resolve @@ -76,10 +112,16 @@ and `[device] module = "platform"` classifies from the `User-Agent`, because the dev server supplies no TLS or HTTP/2 signals. An Edge Cookie module creates and writes its cookie with no identity graph behind it, and `POST /_ts/api/v1/ec/resolve` sets one the same way. `[[proxy.asset_routes]]` -are served from each route's own source with the asset body held whole, with -no limit. The dev server has no Image Optimizer, so startup warns about each -route that enables one, and each request such a route would optimize answers -`502` (see [Asset Routes](./asset-routes)). +are served from each route's own source. The HTTP client passes a streamed +origin body on as it arrives, held to the caller's byte limit if it sets one, +and the dev server's own front end hands a streamed response to the client +chunk by chunk, so a slow client slows the origin read and a client that +leaves drops it. A buffered body is held whole, with no limit. The client's +30 second timeout covers the whole exchange, a streamed body included. At most +256 requests run at once, and later ones wait for a free slot. The dev server +has no Image Optimizer, so startup warns about each route that enables one, +and each request such a route would optimize answers `502` (see +[Asset Routes](./asset-routes)). Axum sends no [auction telemetry](./telemetry#adapter-support), and warns at startup when `[analytics]` selects a module. diff --git a/docs/guide/cloudflare.md b/docs/guide/cloudflare.md index b85c962d89..0005ca6a05 100644 --- a/docs/guide/cloudflare.md +++ b/docs/guide/cloudflare.md @@ -104,8 +104,14 @@ selects run as on Fastly, and `[device] module = "platform"` classifies from the `User-Agent`, because the Worker supplies no TLS or HTTP/2 signals. An Edge Cookie module creates and writes its cookie with no identity graph behind it, and `POST /_ts/api/v1/ec/resolve` sets one the same way. -`[[proxy.asset_routes]]` are served from each route's own source with the -asset body held whole, up to 10 MiB, and a larger one answers `502`. The +`[[proxy.asset_routes]]` are served from each route's own source. The HTTP +client passes a streamed origin body on one runtime chunk at a time, held to +the caller's byte limit if it sets one, and the Worker sends a streamed +response to the client as it is produced. A buffered body is held whole, up +to 10 MiB, and a larger one answers `502`. Because the Workers stream reports +an aborted origin connection as a clean end, a streamed body without a +`Content-Encoding` that ends short of its `Content-Length` ends with an error +instead of reaching the client as complete. The adapter does not translate Image Optimizer metadata into Cloudflare's own image options, so the Worker warns about each route that enables the Image Optimizer, once for each instance (see [Logging](#logging)), and each request diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index b9417d7669..aa439484b8 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -1389,7 +1389,7 @@ A module with settings of its own reads them from its own block, `[robots-txt.]` table with the same four settings. Core applies none of the lifetimes in such a table. They @@ -2459,10 +2486,90 @@ generated configuration places it in. | `ad-tag.google.diagnostics` | `[[serve]]` | For a request that activated diagnostics, writes the bootstrap ahead of the script bundle and the tag that loads the diagnostics module straight after it | | `ads-txt.iabtl` | `[[fetch]]` | Writes the lines `[ads-txt.iabtl]` gives at the end of the publisher's `ads.txt`, on `text/plain` alone | | `headers.geo` | `[[serve]]` | Writes the location the request resolved as the `x-geo-*` response headers, or `x-geo-info-available: false` where none is known. `[headers] modules = ["geo"]` selects it | +| `provenance.encypher` | `[[serve]]` | Signs eligible articles and embedded media on admitted public pages. Shared policy failures leave the page unchanged; article and individual media failures leave only those items unchanged, so other eligible items may still be signed. `[provenance] module = "encypher"` selects it | | `51degrees.client_hints` | `[[serve]]` | Asks the 51Degrees service which client hints the reader's browser can send, writes `Accept-CH` and `Critical-CH` unless the browser already sends those hints, `Permissions-Policy` on the response and the `Delegate-CH` tag in the head for that list, and leaves the names in the request's state | | `robots_txt` | `[[fetch]]` | Makes the `robots.txt` that `[robots-txt]` describes, in place of whatever the origin answers, on `text/plain` and at `/robots.txt` alone | | `service_version` | `[[serve]]` | Writes `X-TS-Version`, the git version the build compiled in, and `X-TS-Service-Version`, the platform's version number of the service, when each is known. Core supplies it, so no section selects it | +In the default synchronous mode, Encypher identifies a reusable signing plan +by canonical article text, policy and credential context. Page URL and title +are deliberately not part of that identity: the matching service deduplicates +equal canonical text, so a reused manifest can retain the first canonical +source URL. Configure the middleware only where that content identity is +correct for the publisher. [Prepared signing](#encypher-prepared-signing) +binds the page URL and admitted variant into every identity instead, so its +results are page-bound. Prepared text identities also bind the page title; +changing only that title does not invalidate an unchanged media item. + +`max_document_bytes` also bounds the final signed HTML. The module counts +inserted marker bytes and the escaped manifest meta element before allocating +the rendered page; a plan that would cross the bound leaves the original page +unchanged. + +Selecting Encypher also installs the media signer for eligible public binary +responses delivered through publisher and asset routes. Response media type +and verified bytes decide eligibility, including on extensionless paths. +Images, audio, video and fonts are candidates; existing provenance and +fragmented stream segments are preserved rather than signed again. +`max_media_bytes` bounds each captured or decoded item, with a maximum of +8,000,000 bytes. + +**Upgrade behavior:** an existing text-signing configuration also enables +bounded media signing after this upgrade. The defaults are 5,000,000 bytes per +item and 8 uncached embedded calls per document. There is no separate bounded +media off switch; removing the module disables its article signing too. +Deploy the matching API first and confirm media upload approval, API capacity +and cold-page latency before upgrading an enabled edge. Repository maintainers +can find the complete settings, validation bounds and rollout contract in +`docs/superpowers/specs/2026-10-24-encypher-provenance-module-design.md`, +section 3. That internal design document is not published on this site. + +`article_selector` accepts a single `.class`, exact `[attr=value]` (optionally +quoted) or tag name, up to 91 characters. Unsupported or malformed syntax +refuses settings admission. An empty selector, or a valid selector absent from +the page, retains the normal article-boundary fallback. + +The HTML middleware signs eligible source `data:` URIs and inline SVG. +For embedded data in CSS, place `provenance.encypher.stylesheet` in a +`[[serve]]` entry with `media_type = "text/css"` and a path narrower than `/`. +The publisher must know that those stylesheets are not loaded with +subresource integrity. To move eligible external references onto configured +asset routes, use the opt-in +[rehosting contract](./asset-routes#rehosting-third-party-files). + +For pages with many embedded images, `[provenance.encypher]` +`max_media_sign_calls_per_document` accepts `1..=128` and defaults to `8`. +It bounds uncached embedded-media signing calls in each HTML or CSS document. +Cache hits do not consume the budget. Once exhausted, remaining items stay +unchanged without a negative cache entry. Raising this value does not increase +`max_hold_ms`, body limits or API rate limits; verify the complete cold page +under the deployed adapter before selecting a larger value. The budget governs +synchronous signing only; in `async_prepared` mode delivery makes no signing +calls. + +`max_large_media_bytes = 0` keeps prepared large-media signing off by default. +In synchronous mode, enabling it requires the matching prepared API and signed +probe, a version-pinned source and a runtime timer that bounds the complete +hashing and signing operation under `max_large_media_ms`. Fastly has no such +total timer, so it refuses this lane even where the API supports the file. Its +first-byte and between-byte timeouts are not substitutes. A refused large-media +signing attempt delivers a fresh origin response without adding provenance. In +`async_prepared` mode the worker hashes outside delivery and delivery only +looks up the published result, so this timer requirement does not apply to the +lookup; the source requirements under +[prepared signing](#encypher-prepared-signing) do. + +Coverage follows the delivered bytes through configured publisher and asset +routes. A page can still load unsigned media directly from a third-party frame +or a script, and scripts can create new inline SVG after the response leaves +Trusted Server. Adding an asset route does not intercept those requests or +establish provenance for those generated nodes. Private stylesheets and +integrity-protected resources retain their refusal rules. Verify terminal +browser responses and dynamic DOM media before claiming complete page coverage; +successful signing of every routed response alone is not that proof. Detached +formats also require retrieval of the emitted manifest URL through the public +deployment, not only verification against a locally supplied sidecar. + An entry that could not do what it says refuses the configuration, both when a deployment is validated and when the settings load. That is a media type no middleware runs on, a path that does not start with `/` or that holds a @@ -2476,6 +2583,317 @@ names it. The sections below give each module's settings, and the integration guides describe what each one does. +### Encypher Prepared Signing + +**Section**: `execution` in `[provenance.encypher]`, with +`[provenance.encypher.preparation]` + +`execution = "synchronous"` is the default and keeps the behavior above. +`execution = "async_prepared"` moves signing out of reader delivery into a +publisher-operated native worker. Delivery looks up a result the worker +published for the exact text or bytes it is about to send, applies it, and +sends every other item unsigned. It never signs, prepares, finalizes or +uploads, and it never falls back to synchronous signing. The worker uses the +same `cwk_` credential, so the claim, host policy and signing identity are +those of synchronous mode; only where signing runs changes. + +Each identity binds the credential context (`api_base_url`, `domain` and the +worker token), the current policy digest, the page address +`https://` without its query, the admitted variant and the +exact item: projected article text with its title and boundary selector, or a +media item's carrier, type and source SHA-256. A changed item, policy or +credential therefore has a new identity and stays unsigned until the worker +prepares it. Article lookups use the cached policy digest; media lookups ask +the policy endpoint live, as synchronous media does, so a revoked credential +stops every published media result. + +```toml +[provenance.encypher] +api_base_url = "https://api.example.com" +domain = "publisher.example.com" +worker_token = "encypher_worker_token" +execution = "async_prepared" + +[provenance.encypher.preparation] +worker_url = "https://prep.publisher.example.com" +loopback_url = "https://publisher.example.com" +deployment_id = "publisher-preparation" +public_host = "publisher.example.com" +signal_secret = "encypher_preparation_signal" +publication_secret = "encypher_preparation_publication" +routes = ["/news/", "/media/"] +reader_trigger = false + +[[provenance.encypher.preparation.variants]] +name = "edition" +values = ["uk", "us"] +``` + +| Field | Default | Contract | +| -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `worker_url` | Required | Worker origin: `https`, or `http` on `127.0.0.1`, `localhost` or `[::1]`; no path, query or user information. Receives hints and source reads | +| `loopback_url` | Required | Own public origin, same rules, and `https://` with an asset cache (below). Snapshots read through it take the reader pipeline | +| `deployment_id` | Required | 1 to 64 lower-case letters, digits or `-`; equal to the worker's | +| `public_host` | Required | `domain` or its `www.` name. Every identity names it, so a reader on another host finds no result | +| `signal_secret` | Required | Secret-store entry name. Its value authorizes reader hints only | +| `publication_secret` | Required | Secret-store entry name. Its value authorizes snapshots, result and marker publication, and public source reads | +| `reader_trigger` | `false` | Send a bounded hint to the worker for unprepared items | +| `admission_ms` | `50` | `10..=200`; the longest a hint waits on an adapter with a timer | +| `routes` | Required | 1 to 64 path prefixes, matched as string prefixes, so `/news` also matches `/newsletter`. Only these paths are snapshotted or hinted | +| `variants` | `[]` | Up to 8 query parameters, each with 1 to 32 values; names and values are 1 to 64 letters, digits, `-`, `_` or `.` | + +`async_prepared` without the table, a `public_host` outside the claim, or +`reader_trigger = true` on an adapter that offers neither a timer nor an +after-response lifetime refuses startup. + +When `[asset-cache]` selects a module, `loopback_url` must be exactly +`https://`. The asset cache keeps each reader copy under the host +and scheme the reader asked, so a snapshot read through any other origin +would miss the copy readers get and prepare content they are not served. A +trailing `/` or the default port, as in `https://publisher.example.com:443/`, +is accepted and read as `https://publisher.example.com`. Any other host, `http`, +another port, user information, a path, a query or a fragment refuses +`ts config validate` and startup. Synchronous mode, and `async_prepared` +without an asset cache, keep the general rule above, so local development can +still read snapshots from `http://127.0.0.1:`. + +Results and retirement markers live +in the runtime KV store as `encypher:prep:v1:result:` and +`encypher:prep:v1:retired:`, so the delivery instance needs runtime +KV (`TRUSTED_SERVER_KV_PATH` on Axum). The module also serves +`/_encypher/preparation/snapshot`, `/_encypher/preparation/results/` +and `/_encypher/preparation/markers/` to the worker, authenticated +by the publication secret, and the public +`/_encypher/preparation/source/`. + +Prepared text signing and recovery require `"cache_scope": "exact_request"` +alongside the frozen policy digest. The API then reuses only a result for +that exact request, including its title, page URL and boundary selector, +rather than one with the same text and rights alone. The worker retains +this scope in every publication envelope; missing or unknown scopes are +refused, never treated as legacy signing. Synchronous signing keeps its +existing request and cache behavior. + +The text identity hash domain is `encypher-preparation/2:text`. Results made +under the former text cache contract cannot satisfy current reader lookups +or be published under an old identity. The preparation protocol and media +identities are unchanged, so this change alone does not require media to +be signed again. Upgrade the worker and delivery instance together against +an API that supports exact-request cache isolation. Retained legacy text +envelopes without the required scope also fail closed during recovery; +they are not migrated by assigning a scope to an old receipt. + +#### Run the worker + +```bash +trusted-server-axum preparation-worker --config worker.toml +``` + +Start from `crates/trusted-server-preparation-worker/worker.example.toml`. +`deployment_id`, `api_base_url`, `domain`, `public_host`, `article_selector`, +`min_text_length`, `max_media_bytes` and `variants` must equal the delivery +settings. `edge_url` is the delivery origin whose `/_encypher/preparation/` +routes the worker calls. `[secrets]` names environment variables, never +values: `worker_token` (the `cwk_` credential), `signal_secret` and +`publication_secret` (equal to the delivery secret-store values) and +`operator_secret` (for `ts preparation`). The four values must differ. The +loader also requires `workers` `1..=4`, `max_snapshot_bytes` at most 64 MiB and +not below `max_media_bytes`, `max_preparation_seconds` `1..=3600`, +`scan_per_minute` `1..=128`, `scan_interval_seconds` `60..=900`, and at most +1,024 `[[inventory]]` entries. Registered inventory capacity is the lesser of +1,024 and `scan_per_minute` times the whole minutes in +`scan_interval_seconds`. At one target per minute and 900 seconds, capacity +is 15 targets; the defaults of 128 and 900 retain the 1,024-target limit. +The same capacity covers distinct configured targets, `--register` additions +and retained inventory. Startup refuses a store above the configured capacity +without deleting its records; raise the scan capacity to reopen it. + +A second process cannot open `store_dir/preparation.redb`, and the worker +refuses to start without it. `GET /v1/preparation/ready` answers `503` until +unfinished jobs are recovered, the delivery instance accepts the publication +secret, the API answers a policy for `https:///`, and a signed +probe release loads: a fresh store fetches one through the API's media +capabilities, and a store with retained work reloads its own. The worker +retries every 10 seconds, logging `preparation: not ready` with the failing +codes, and prepares nothing until ready. It drains on `SIGINT`; any other stop +is a crash that the next start recovers by reconciling byte reservations and +reclaiming unfinished claims. Delivery must reach `worker_url`, and the worker +must reach `edge_url` and `api_base_url`. The worker's warm, retire and status +routes require the operator secret. + +#### Supply the inventory + +The worker prepares only the targets it is given. It does not crawl the +origin, read sitemaps or discover uploads. A target is a path and an optional +variant. Supply the full inventory through `[[inventory]]` entries, which are +registered at every start, through `ts preparation warm --register`, or from a +publication hook that runs `ts preparation warm` for each changed path. + +```bash +export TS_PREPARATION_OPERATOR_SECRET='' +ts preparation warm --worker-url https://prep.publisher.example.com --register \ + --target /news/harbor-vote --target /news/budget-2027 --target /media/flood.jpg +``` + +The CLI has no file option. For a publisher-supplied list, one path per line: + +```bash +args=() +while IFS= read -r path; do args+=(--target "$path"); done < targets.txt +ts preparation warm --worker-url https://prep.publisher.example.com --register "${args[@]}" +``` + +One call takes up to 1,024 targets (`413 too_many_targets` above that). Every +target in a call shares `--variant`, which must be the canonical admitted form, +pairs sorted by name such as `--variant edition=uk`; run one call per variant. +The worker URL must be `https`, or `http` on a loopback host. The command +prints one outcome per target: `queued`, `coalesced` (merged into a waiting +job), `damped` (published, retired or suppressed), `queue_full`, or +`not_admitted` (a malformed path or a variant the worker's schema refuses). It +exits nonzero when any target was `queue_full` or `not_admitted`; repeating the +call is safe. `--retry` first clears the targets' suppressed outcomes. +`--register` records the admitted targets before queueing them, so a +`queue_full` target stays registered. Registration only adds: no command +removes a target, and deleting an `[[inventory]]` entry does not unregister +it. A full inventory answers `409 inventory_full` without partially registering +the batch. Duplicate targets do not consume additional capacity. + +Once a minute the worker queues up to `scan_per_minute` registered targets not +reconciled within about `scan_interval_seconds`, from a persisted cursor. A +reconciliation prepares only items its target's previous snapshot also held, +so a new or changed item is prepared on the second pass that sees it; a warm +prepares the current snapshot at once. + +A page target covers its article text and the eligible `data:` URI media and +inline SVG in that page. A stylesheet target under a +`provenance.encypher.stylesheet` entry covers its embedded `data:` media. A +media target covers that file. Images, video, fonts and stylesheets that a page +links by URL are separate resources: each needs its own target, or a reader +request that hints it. A target must lie under `routes` and reach a +`[[serve]]` entry that names the module or, for media, a publisher or asset +route; otherwise the snapshot is refused as `target_not_configured` or +`snapshot_unobserved`. + +#### Reader hints + +With `reader_trigger = false`, delivery only looks up results. With `true`, a +document or standalone item with misses sends one hint of at most 8 KiB, for a +path under `routes` with an admitted variant. A hint is never retried or +stored on the edge; only the worker's admission is durable, and warming and +reconciliation recover a lost hint. A hint prepares only the items the reader +saw, and only while a fresh snapshot still holds them; otherwise the item is +recorded as `source_unstable` for five minutes. + +| Adapter | Hint lifetime | +| ------------------------------- | ---------------------------------------------------------------------------------------------- | +| Axum | Real timer; the hint waits at most `admission_ms` | +| Cloudflare Workers (WASM) | Real timer; the hint waits at most `admission_ms`. `waitUntil` is not used | +| Spin (WASM) | Real timer; the hint waits at most `admission_ms` | +| Fastly | No timer. The hint is sent after the response, at most 8 per response, best effort | +| Native Cloudflare or Spin build | No timer and no after-response lifetime: `reader_trigger = true` refuses startup; `false` runs | + +Delivery still reads the runtime KV store and the policy endpoint, so no fixed +latency is promised. Where the adapter has a timer, `max_hold_ms` bounds that +work; on Fastly only backend timeouts do. Only the Axum adapter has run this +mode against a live worker and API. + +#### Media + +The worker hashes media locally and never uploads it. All media preparation +uses worker contract version 2, so it needs an API that advertises that +version; text does not. Embedded `data:` media and inline SVG up to +`max_media_bytes` get an embedded manifest, and their source is named by a +digest-addressed publisher snapshot at +`https:///_encypher/preparation/source/`. A bounded +standalone file keeps its bytes and gets a detached manifest named by a `Link` +header. Its source is named by its fetched `https` origin URL where that URL +has no query, and by a publisher snapshot otherwise. Delivery serves a +snapshot only after the page and asset passed public admission, as the exact +bytes with `Content-Disposition: attachment` and a sandbox CSP. A copy of a +standalone file taken without that `Link` carries no provenance. + +Large items need `max_large_media_bytes` above zero on both delivery and +worker. A source larger than the whole-snapshot bound is read in ranges pinned +to its strong `ETag` and length, hashed whole under `store_dir/large` and +signed with a detached manifest. Delivery decorates the origin's own answer +when the configured origin, type, length, `ETag` and query match. The fetched +source URL must be `https` without a query (`source_reference_required` +otherwise), so a query-forwarding route cannot use this lane, and the reader's +raw query must equal its canonical admitted variant. Reuse trusts the origin's +strong validators; an `ETag` is not a cryptographic proof. + +Privacy and existing provenance fail closed. A snapshot the origin marked +private (`source_private`), one with its own manifest `Link` +(`source_has_provenance_link`), `no-transform` (`no_transform`) or a content +coding (`content_encoded`) is refused and not prepared. + +#### Status and capacity + +```bash +ts preparation status --worker-url https://prep.publisher.example.com +``` + +The answer has `ready`; `counts` of `pending` and `processing` jobs, +`artifacts`, `publication_pending`, `publication_acknowledged`, +`retirement_pending`, `retirement_acknowledged`, `inventory` and +`reserved_bytes`; `active_refusals_by_code`; and `capacity`: 1,024 `jobs`, of +which 768 `reader_jobs` (256 slots are kept from reader hints), 8 MiB +`queue_bytes`, 6 MiB `reader_queue_bytes`, the configured scan capacity as +`inventory_targets` (at most 1,024), and `artifact_bytes`, the configured +`artifact_quota_bytes`. + +A transient failure retries with backoff, up to six attempts within 24 hours, +then is suppressed for one hour. An integrity, privacy, unsupported or +existing-provenance refusal stays until the item's identity changes or +`warm --retry`. `active_refusals_by_code` counts these retained outcomes. It +also keeps one diagnostic outcome per extraction refusal reason and exact page +version, such as `malformed_data_uri`, while valid siblings still prepare. The +counts record what extraction saw; they are not proof that every medium on a +page was covered, because linked media appear only when targeted. The worker +logs each item refusal as `preparation: item refused kind=... code=...`. + +`artifact_quota_bytes` is a hard reservation over sources, temporary large +files, probes, plans, outputs and their metadata, taken before work starts. A +full quota refuses new work with `quota_exhausted` and never evicts retained +recovery material; raise the quota and restart, or retire identities. A full +queue answers `queue_full`. + +#### Retirement + +```bash +ts preparation retire --worker-url https://prep.publisher.example.com --identity <64 hex> +``` + +An identity is the suffix of its delivery KV key +`encypher:prep:v1:result:`. Retirement records a tombstone, raises +the retirement epoch, drops the item's artifact and plan, and queues a marker. +Each one-minute housekeeping pass attempts up to 16 queued markers until the +delivery instance acknowledges them. Bulk retirement and failed deliveries +can leave markers pending for multiple passes. The command answers `marker` +`pending` or `acknowledged`, and `status` counts both. A reader refuses a +result whose marker is present or unreadable. Acknowledgement means the +delivery KV write succeeded, not that +every KV replica, downstream cache or downloaded copy changed. Hints, warming +and reconciliation never prepare a retired identity again; changed content has +a new identity. + +Policy withdrawal is separate from an identity's retirement marker. Text +lookups retain the existing `policy_ttl_seconds` policy cache; media lookups +request current policy. Neither path makes withdrawal retroactive for copies +already downloaded. + +#### Durable state + +`store_dir` holds `preparation.redb` and the retained files: the only durable +copy of the queue, inventory, outcomes, tombstones, marker outbox and +artifacts. When a hint, warm or reconciliation finds a published result +missing from the delivery KV store, the worker republishes it from its +artifact without signing again. Back up `store_dir` as one unit while the +worker is stopped, restore it to the same path, and do not run a restored copy +beside the original. Never delete `encypher:prep:v1:retired:` entries from the +delivery KV store. Changing the worker token, `api_base_url` or `domain` +changes every identity, so all content is prepared again. + ### DataDome Integration **Section**: `[bot-protection.datadome]` diff --git a/docs/guide/fastly.md b/docs/guide/fastly.md index 4860c60990..0d78e68e3d 100644 --- a/docs/guide/fastly.md +++ b/docs/guide/fastly.md @@ -13,6 +13,33 @@ The row above summarizes the response does not prove that configuration loaded: Fastly serves `/health` before it constructs the application. +## Request deadline boundary + +Fastly provides no guest timer to `RuntimeServices`. A module such as +`provenance.encypher` therefore cannot enforce `max_hold_ms` as a total elapsed +deadline on this adapter. The configured backend timeout bounds time to the +first response byte and the gap between response bytes. It does not bound the +whole exchange: a peer that sends each byte before the gap timeout can keep +the request open longer than `max_hold_ms`. Fastly KV calls also have no +module-level elapsed deadline. + +Encypher still limits the signing API response while Fastly reads it. The +buffered path reads the host body into an 8 KiB guest buffer and checks the +cumulative count before appending. The streaming path checks the count before +yielding each 8 KiB chunk. Fastly's host `select` operation does not preserve +the per-request limit metadata, so the adapter rejects bounded or streaming +pending handles passed to `select`; wait for those handles directly. + +With Encypher's `execution = "async_prepared"`, delivery makes no signing +call. Lookups of published results and the live media policy check have the +transport bounds above and no elapsed deadline. A large-media lookup needs no +timer, because the worker already hashed the source. With +`reader_trigger = true`, a hint is handed to the after-response lifetime: the +adapter sends at most 8 handed-over requests of at most 8 KiB each after the +reader's response, best effort and never retried. This hint path has not run +against a live worker. See +[Encypher prepared signing](./configuration#encypher-prepared-signing). + ## Create a Fastly Account 1. Go to [manage.fastly.com](https://manage.fastly.com) and create an account if you don't have one diff --git a/docs/guide/spin.md b/docs/guide/spin.md index 47ee4a3967..b1e597fbb3 100644 --- a/docs/guide/spin.md +++ b/docs/guide/spin.md @@ -113,8 +113,11 @@ refused at startup. The modules' request filters and the device module classifies from the `User-Agent`, because Spin supplies no TLS or HTTP/2 signals. An Edge Cookie module creates and writes its cookie with no identity graph behind it, and `POST /_ts/api/v1/ec/resolve` sets one the same way. -`[[proxy.asset_routes]]` are served from each route's own source with the -asset body held whole, up to 8 MiB, and a larger one answers `502`. Spin has +`[[proxy.asset_routes]]` are served from each route's own source. The HTTP +client passes a streamed origin body on one host chunk at a time, exactly as +the origin encoded it and held to the caller's byte limit if it sets one, and +the component sends a streamed response to the client as it is produced. A +buffered body is held whole, up to 8 MiB, and a larger one answers `502`. Spin has no Image Optimizer, so the component warns about each route that enables one, once for each instance (see [Logging](#logging)), and each request such a route would optimize answers `502` (see [Asset Routes](./asset-routes)). Spin @@ -123,6 +126,77 @@ sends the address's own host as the `Host`, so a request to a source whose before its origin is asked, and the component logs the refusal as an error that names both hosts. +For bounded outbound calls such as Encypher signing, the adapter consumes +Spin SDK 6's `IncomingBodyExt::stream()` rather than `bytes()`. It checks each +host-created chunk before copying it into the buffered response and applies +the same bound again to gzip or Brotli output. The host chunk already exists +when guest code receives it, but the adapter does not collect the whole body +before enforcing the bound. A streamed response reads the same stream one +chunk at a time and decodes nothing, and a streamed body that ends short of +its `Content-Length` ends with an error. + +The component's entry point dispatches through `EdgeZero`'s router itself, +where it called `edgezero_adapter_spin::run_app`, because that wrapper +collects a streamed response into one buffer, capped at 16 MiB, before the +host sees it. It opens the declared KV stores as `run_app` does, with the +same `EDGEZERO__STORES__KV____NAME` and `MAX_LIST_KEYS` settings, and a +store it cannot open fails the request. + +### Retaining and removing Encypher cache entries + +Spin's key-value API has no entry TTL. Encypher records an `expires_at` value, +ignores an expired entry and deletes it best effort when that key is read +again. This is not garbage collection: a content key that no request reads +again remains in `default`. + +The active namespaces are `encypher:plan:v4:`, +`encypher:plan-skip:v4:`, `encypher:policy:v3:` and +`encypher:policy-skip:v3:`. The same store contains +`trusted_server_config` and may contain other modules' data. Never delete the +whole store to remove Encypher entries. In particular, the current +`spin cloud key-value` plugin can delete a store but cannot enumerate and +delete individual entries safely. + +With `execution = "async_prepared"`, the same store also holds published +results under `encypher:prep:v1:result:` and retirement markers under +`encypher:prep:v1:retired:`; probe releases for result verification are cached +under `encypher:probe:v1:`, as in the large-media lane. Results and markers +carry no `expires_at`. Never delete a retirement marker: a reader refuses a +retired result only while its marker is present. A result the preparation +worker still retains is republished when a hint, warm or reconciliation finds +it missing. The WASM component has a real timer, so it accepts +`reader_trigger = true`; a native host build has none and refuses it at +startup. + +This cache split retires the old `encypher:v3:` article prefix and +`encypher:policy:v2:` policy prefix. They are deletion candidates only after +the same checks below. Before deleting `encypher:v3:`, wait the greater of +`plan_ttl_seconds` and `negative_ttl_seconds`; before deleting +`encypher:policy:v2:`, wait `policy_ttl_seconds`, in both cases plus the +rollback window. + +Use the backing provider's item-level API, or a reviewed one-shot maintenance +component that calls `list_keys_page(prefix)` and `delete`, with this +procedure: + +1. Rotate to a new versioned namespace and quiesce writes while taking the + inventory. Do not proceed while a live binary can still write the retired + prefix. +2. Export or back up `default` and record `trusted_server_config` separately. +3. Enumerate and preview only the exact retired prefix. Never use an empty + prefix and never match `trusted_server_config`. +4. Wait through the namespace's configured lifetime plus the deployment's + rollback window: `plan_ttl_seconds` for `plan`, `negative_ttl_seconds` for + `plan-skip`, `policy_ttl_seconds` for `policy`, and the lesser of + `policy_ttl_seconds` and `negative_ttl_seconds` for `policy-skip`. +5. Delete only the previewed old-prefix entries. Verify + `trusted_server_config` is unchanged and sample active-prefix entries still + read before resuming writes. + +The KV interface has no compare-and-delete operation. If writes cannot be +quiesced, retain uncertain entries rather than risk deleting a concurrent +replacement. + Spin sends no [auction telemetry](./telemetry#adapter-support), and warns at startup when `[analytics]` selects a module. diff --git a/docs/superpowers/specs/2026-10-24-encypher-provenance-module-design.md b/docs/superpowers/specs/2026-10-24-encypher-provenance-module-design.md new file mode 100644 index 0000000000..6d3a512ba9 --- /dev/null +++ b/docs/superpowers/specs/2026-10-24-encypher-provenance-module-design.md @@ -0,0 +1,406 @@ +# Encypher text and routed-media provenance as a module + +Status: implemented scope on `encypher/all-media-provenance`, extending the +text-provenance module described below. Local Axum, real HTTPS API and browser +verification covers the routed subset (section 4). Opt-in asynchronous +prepared execution (section 2.6) is implemented on +`encypher/async-prepared-signing`. Full repository landing gates remain +separate. This document claims neither complete page-media coverage nor a +production deployment. + +## 1. Problem + +A publisher who wants proof of authorship carried inside article text needs a +signing step at the edge, after the page is assembled and before it reaches the +reader. Encypher's earlier contribution did this as an integration inside core, +with core changes of its own: an HTML stream processor factory, a request +context type, an asynchronous finish for stream processors, a platform timer +and runtime KV on each adapter. The stack moves every vendor out of core and +lets a module change a page through a middleware alone, so the contribution has +to move onto those interfaces, and the core seams it needs have to be neutral. + +## 2. Design + +### 2.1 What core gains, for any module + +1. **An awaited end of document.** `StreamProcessor::async_finish` lets a + processor finish its last output asynchronously, with a deadline. + `StreamProcessor::finish` ends a processor once; a processor made of others + ends each in order, so each is handed what the one before it finished with. + Every publisher path that can await ends its processor this way: the + no-hold pipeline, both auction-hold loops, the serve pass on a shared + template and the lazy stream. A body read whole takes the same chunk + pipeline as a streamed one, and the synchronous `stream_publisher_body` + path is removed, so no path silently skips a finish. A finish that outlives + its deadline on a runtime with a timer is dropped and the held bytes go on. +2. **`PlatformTimer`**, optional on `RuntimeServices`. The Cloudflare, Spin and + Axum adapters provide one. Fastly provides none: its backend setting bounds + the first byte and each between-byte gap, not total elapsed work. A peer can + keep a request open beyond `max_hold_ms` by sending each byte before the gap + timeout, and Fastly KV calls have no module deadline. +3. **Response facts for middleware.** `MiddlewareContext` carries the + response `status` and `origin_private`, whether the origin marked its + response as one reader's by a cookie or a `Cache-Control` that forbids + shared storage, read before core writes its own cache policy. +4. **Runtime KV where the stack left it unavailable.** Fastly opens the store + linked as `trusted_server_kv`; Axum opens a persistent store at + `TRUSTED_SERVER_KV_PATH`, running each operation on the blocking pool so a + timer raced against it can fire. Cloudflare and Spin already open their + declared store on the stack and are unchanged. +5. **An optional outbound response-body bound.** A caller can ask every + adapter to reject a response before buffering or yielding more than a + chosen byte count. Axum reads reqwest chunks, Cloudflare reads its + `ByteStream`, Spin reads `IncomingBodyExt::stream`, and Fastly reads + `fastly::Body`. Axum, Cloudflare and Spin can inspect a transport or + host-created chunk only after that one chunk exists, but none collects the + complete response before enforcing the bound. Spin applies the same limit + to gzip and Brotli output. + Fastly preserves the limit on a direct pending wait and refuses bounded + handles through `select`, whose host API does not preserve wrapper metadata. +6. **Original-source protection**, default off on `Middleware`. The applicable + fetch and serve chains supply classifiers through `source_protection`; core + knows only the exact byte ranges they return. Before any HTML transformation, + a selected policy holds the complete original under the publisher body limit. + Opaque comment markers replace protected slices while core and middleware + work. The pass checks marker collision, presence, order and uniqueness, then + restores the original slices before any byte leaves the pass or enters the + shared-template cache. Cached reader copies take the same protection around + serve transformations. Invalid ranges, marker damage and limit excess refuse + the pass. A policy applies even when the module's later request gate declines + signing; it is selected by the actual path/media-type chains, not by rehosting. + This bounds memory and delays the first transformed byte until the full + document has been classified and restored. + The transformation view contains opaque comments, not the protected source. + Raw whole-HTML signing/digest middleware must not bind that intermediate + view and assume restoration preserves its binding. Encypher's article plan + key and signing payload use canonical text, which ignores SVGs and comments; + the random marker therefore enters neither that text nor its cache identity. + This contract does not establish compatibility for arbitrary future + middleware that binds the complete HTML byte sequence. + +### 2.2 The module + +`crates/provenance/encypher`, `provenance.encypher`, selected by +`[provenance] module = "encypher"` and placed by a `[[serve]]` entry. + +- **Preparation** applies the gates with no I/O: `GET`, status `200`, an + origin response that is not one reader's, and a host equal to `domain` with + or without `www.`. API, feed, admin, well-known and asset paths are left out + of the chain by `handles_path`. +- **Create** returns a stream processor that holds the document up to + `max_document_bytes`, and signs in its asynchronous finish: policy digest, + plan cache, sign call, cache writes, render. Before allocating rendered + output, it checks the exact marker and escaped manifest expansion against + the same bound. A shared policy failure leaves the whole document unchanged. + Article and individual media failures are isolated: failed items stay + unchanged while other eligible parts may still be signed. +- **Serve phase only**, so a failed signing is never stored as the shared + template every reader receives. +- **Cache integrity** keeps positive plans and policy digests apart from + negative results, reads a positive first, and uses versioned namespaces: + `encypher:plan:v4:`, `encypher:plan-skip:v4:`, + `encypher:policy:v3:` and `encypher:policy-skip:v3:`. Malformed or + structurally invalid entries are deleted best effort. An alignment failure + for one HTML representation does not delete a valid plan. +- **Content identity** in the default synchronous mode remains the policy + digest, credential context and canonical article-text hash, not the page + URL. The matching Encypher server contract deliberately deduplicates equal + canonical text across URL or title changes. A reused plan's manifest URL can + therefore retain the first canonical source URL. Prepared text identities + (section 2.6) also bind the page URL, title and admitted variant. +- **Original SVG preservation** uses `embedded::scan_html` and its existing + namespace-aware provenance/equivalence classifier for both source protection + and signing decisions. A carrier or provenance reference after an editable + descendant protects the complete original outer SVG. Protected descendants + never reach core URL/text rewriting or middleware element, text or signing + handlers. Unclosed or structurally uncertain ranges remain opaque. Unsigned + SVGs still pass through the normal normalization and signing path. No rehost + route is required for protection. + +### 2.3 Routed and embedded media + +The selected module also supplies a vendor-neutral `MediaSigner` to core. +Publisher and asset routes can sign bounded public media responses recognized +by their response type and verified bytes, including extensionless paths. +Images, audio, video and fonts are candidates, not a promise that every format +is supported. Existing embedded, XMP-referenced and HTTP-linked provenance is +retained rather than replaced. Fragmented ISO BMFF init and media segments +remain unchanged; a progressive MP4 can take the normal signing path. + +The HTML serve processor signs eligible `data:` URIs and inline SVG in the +source document. An unsigned inline SVG is signed only as a standalone XML +document equivalent to the browser's HTML-parsed tree. A path-scoped +`provenance.encypher.stylesheet` serve entry signs eligible embedded data in +CSS. It must cover only stylesheets the publisher knows are not loaded with +subresource integrity. + +An asset route's opt-in `rehost_page_references` moves eligible source HTML +media and stylesheet references onto that route. The route must preserve the +exact upstream resource identity; moved CSS resolves its own references +against its upstream URL. Private, integrity-protected and otherwise unsafe +resources keep their existing refusal behavior. The +[asset-route guide](../../guide/asset-routes.md) defines routing, relocation, +freshness and Image Optimizer behavior. + +`max_media_bytes` bounds each captured or decoded item; +`max_media_sign_calls_per_document` bounds uncached embedded-media calls +(default `8`, allowed `1..=128`). Cache hits use no call budget. Exhaustion +leaves remaining items unchanged without a negative cache entry. Document +time and output-size limits still apply. + +The prepared large-media lane is opt-in (`max_large_media_bytes = 0` by +default). It hashes a version-pinned source in bounded chunks and requests +detached provenance without uploading or buffering the complete media. +In synchronous mode, admission requires the source/version checks, matching +prepared API and signed probe, streaming support, and one enforceable +wall-clock budget for hashing and signing. Fastly supplies no such total +timer, so this lane stays closed there even when the API can sign the source. +A refusal delivers an unsigned fresh origin response; it does not weaken +deadline or source-binding guards. In prepared execution the worker hashes and +delivery only looks up the result (section 2.6). + +### 2.4 Permission contract + +The module requires no permission and reads no gated value. It sends the +visible article text or eligible bounded media bytes, together with the +reader-facing address without its query and the applicable source metadata. +The prepared lane sends source identity and digest instead of the whole file. +A page whose origin set a cookie or sent a `Cache-Control` that forbids shared +storage is never sent; a test covers this. The module cannot detect a +personalized article that carries neither signal, so it does not exclude +reader data automatically. The deployer places the middleware only on public +article routes, through the `[[serve]]` entry's `path`, and has the origin mark +every personalized response private. The worker token is a module secret +setting, resolved from the secret store and never logged. + +### 2.5 What the port removes + +The `enabled` flag (selection runs the module), `path_prefixes` (the entry's +`path` covers it), the `[integrations.encypher]` table, the HTML stream +processor factory and request context, the shared-template refusal (the serve +phase runs on each reader's assembled copy), and the synchronous publisher +body path. + +### 2.6 Asynchronous prepared execution + +`execution = "async_prepared"`, with `[provenance.encypher.preparation]`, is +opt-in; `synchronous` stays the default. A publisher-operated native worker +(`crates/trusted-server-preparation-worker`, launched as +`trusted-server-axum preparation-worker --config worker.toml`) owns one +exclusive redb store: queue, fenced leases, bounded retries, byte quota, +retained sources, probes and results, inventory reconciliation and a +retirement outbox. It reads snapshots through the delivery instance's reader +pipeline, extracts items with the same code delivery runs, hashes media +locally under worker contract version 2, signs through the API, and asks the +delivery instance to publish each result. Delivery recomputes the identity, +recovers the exact receipt from the API without signing and checks it against +the actual source before storing it under an immutable identity in runtime KV. + +Reader delivery only looks up results bound to the exact item, page URL, +admitted variant, policy and credential context. Text identities also bind +the page title and boundary selector. Delivery never signs, prepares, +finalizes, uploads or falls back to synchronous signing; a miss is delivered +unsigned. Optional reader hints (`reader_trigger`) are bounded and +best effort: Axum, and Cloudflare and Spin WASM builds, race the hint against +a real timer (`admission_ms`); Fastly sends it after the response; Cloudflare +does not use `waitUntil`; native Cloudflare and Spin builds refuse +`reader_trigger = true`. Coverage comes from supplied targets, registered +inventory and publication-triggered warming, not from crawling or upload +discovery. Large sources need a query-free `https` fetched URL and strong +validators; no path uploads source bytes. The +[configuration guide](../../guide/configuration.md#encypher-prepared-signing) +holds the operator contract. + +## 3. Configuration + +```toml +[provenance] +module = "encypher" + +[provenance.encypher] +api_base_url = "https://api.example.com" +domain = "publisher.example.com" +worker_token = "encypher_worker_token" + +[[serve]] +media_type = "text/html" +path = "/news/" +middleware = ["provenance.encypher"] +``` + +The settings below belong to `[provenance.encypher]`. Unknown keys are refused. + +| Setting | Default | Validation and effect | +| ----------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `api_base_url` | Required | HTTPS URL with a host, no query or fragment | +| `domain` | Required | Bare lowercase hostname without `www.` | +| `worker_token` | Required | Secret-store entry name, resolved when settings load | +| `article_selector` | Empty | At most 91 characters; simple `.class`, `[attr=value]` or tag selector; malformed syntax is refused. Empty or valid-but-absent overrides retain the normal boundary fallback | +| `min_text_length` | `50` | `50..=50000` Unicode scalars | +| `max_document_bytes` | `5000000` | At least `1024`; effective bound also capped by `publisher.max_buffered_body_bytes` | +| `timeout_ms` | `3000` | `250..=10000`; API first-byte timeout | +| `max_hold_ms` | `5000` | `500..=15000`, strictly greater than `timeout_ms`; total document hold where a timer exists | +| `plan_ttl_seconds` | `604800` | `60..=31536000` | +| `policy_ttl_seconds` | `300` | `60..=300` | +| `negative_ttl_seconds` | `300` | `60..=86400` | +| `max_media_bytes` | `5000000` | `1024..=8000000`; bounded binary or decoded embedded item | +| `max_media_sign_calls_per_document` | `8` | `1..=128`; uncached embedded calls per HTML/CSS pass | +| `max_large_media_bytes` | `0` | Disabled at `0`; otherwise `16777216..=9007199246352383` bytes | +| `max_large_media_ms` | `60000` | `5000..=600000`; one budget for namespace resolution, cache lookup and newly launched hash/sign work | + +`execution` defaults to `synchronous`; `async_prepared` requires +`[provenance.encypher.preparation]` (section 2.6). + +### Upgrade and rollout + +An existing deployment selecting the Encypher module begins sending eligible +bounded media to the signing API after this upgrade, using the defaults above. +Embedded media on admitted HTML is included. There is no separate bounded-media +disable setting: deselecting the module also disables its article signing. +The large-media lane remains default-off. + +Deploy the matching API first. Before upgrading an enabled edge, check media +upload approval, API capacity, route scope, document/output limits and cold-page +latency. Verify terminal media bytes and rendering under the deployed adapter. +Rehosting and stylesheet processing require their explicit route/serve entries; +the upgrade does not intercept direct third-party or script-generated media. + +## 4. Tests + +- Core: `finish` ends each processor after the one before it has finished; a + finish past its deadline passes the held bytes on; a streamed body without + an auction takes the chunk pipeline; the parser-confirmed auction hold + injects bids before `` into a document a middleware processor held + and finished asynchronously. +- Module: the request gates, policy and sign flows, credential-bound cache + bypass, positive/negative cache races, malformed-entry recovery, output + expansion at the buffered publisher limit, configuration bounds, the + missing-entry refusal, path exclusions, and publisher-path signing through + both the buffered and streaming finalizer against a stub origin and API. The + tests also verify the address without its query and the private-page gate. + Original-source regressions cover origin-host attributes and text inside a + protected SVG, late carriers/references, nested embedded media, surrounding + URL changes, self-closing protected SVG beside unsigned normalized/signable + SVG, bounded refusal, and real cold/warm shared-template publication through + both finalizers. Core tests cover fetch and serve descendant handlers plus + marker loss, duplication, reordering, alteration, invalid ranges and bounds. +- Adapters: the Fastly store opens when linked; Axum KV persists and the + timer fires while other work is pending. Core's test client rejects an + over-limit body on direct and asynchronous sends. All four adapter + collectors exercise exact-limit and over-limit bodies; Axum uses a chunked + TCP response, Cloudflare and Spin use multi-chunk host-stream equivalents, + and Fastly covers buffered reads and a crossing chunk yielded by a stream. +- Media and routing regressions cover binary signing and existing-provenance + preservation, embedded data and SVG equivalence, prepared-source version and + deadline refusal, rehosting identity and HTML base resolution, CSS relocation + and freshness, and prefix-aware reader addresses. + +### 4.1 Runtime smoke + +Recorded local verification on 2026-10-27 used Axum, the real HTTPS signing API +and Chromium: + +- All 148 edge-served browser media responses in the captured replay verified; + 107 sequential network responses, 138 source-inline SVGs and 186 data URIs + also verified in their respective checks. +- Exact-source first/repeat checks and four concurrent API probes verified. + The API recorded 214 media requests and 280 policy requests, all `200`, with + no `500` or `429` responses. +- Six desktop/mobile browser flows covering control, generic-host and + prefix-addressed serving rendered equivalently, with no broken images. + +Native core and module runs reported 3,149 and 210 passing tests respectively. +Native/WASM adapter builds, clippy and Rust formatting checks were also +recorded. These are scoped receipts, not proof that every command in the +current [AGENTS.md CI gates](../../../AGENTS.md#ci-gates) has run against the +final landing tree. Final gate execution belongs to the landing operator. + +### 4.2 Verification limits + +The browser run still loaded 76 direct external responses and two +script-created SVGs unsigned. These requests and nodes never entered the +configured signing paths. No new integration, exclusion or interception of +those resources is part of this scope. + +Prepared large-media API signing has separate proof, but Fastly cannot +enforce the complete hash-and-sign deadline and therefore refuses that lane. +Neither the local Axum run nor Viceroy proves deployed Fastly Image Optimizer +behavior. Verification using a locally supplied detached manifest does not +prove that the emitted manifest URL is publicly retrievable. Those are +distinct deployment checks, not results established by this run. + +Prepared execution has actual-network evidence only on the native Axum +delivery instance with the native worker, a real API and signed probe. The +Fastly, Cloudflare and Spin hint paths have not run against a live worker. + +## 5. Limits and operator obligations + +1. Spin's declared KV store is `default`, which also holds the settings. + Encypher entries have the four exact namespaces above. Spin KV supplies no + TTL, so `expires_at` prevents stale reads and on-read deletion removes + encountered stale entries, but abandoned content keys need the + namespace-retirement procedure in the Spin deployment guide. +2. In synchronous mode, holding the page delays its first byte by the signing + time on a cache miss. `max_hold_ms` is a total module bound on Cloudflare, + Spin and Axum. Fastly has only first-byte and between-byte transport bounds, + as described above. Prepared execution holds no signing work, but its + lookups have no fixed latency bound either. +3. The module skips pages the origin marks private, but cannot detect other + personalized pages. A deployment places it on public article routes. +4. Routes cover only bytes that pass through them. Direct third-party frame or + script requests and SVG created later by scripts require separate evidence + and are not covered by this implementation. +5. Enabling a large-media size limit does not make the synchronous lane + available without an enforceable total deadline. Fastly remains closed; do + not substitute its first-byte or between-byte timeout for that deadline. +6. A detached-format deployment must prove public retrieval of its emitted + manifest URL. A local sidecar backchannel proves neither public routing nor + deployment availability. +7. Prepared execution covers only supplied targets and hinted items. A page + target covers its text and inline media; linked media needs its own target + or hint. Worker refusal counts are retained outcomes, not proof of page + coverage. + +## 6. Landing evidence checklist + +- [x] Implement routed binary signing, source HTML embedded-media signing and + opt-in rehosting with existing privacy, integrity and byte-identity guards. +- [x] Record local real-API and browser results for the implemented subset. +- [x] Keep direct external and script-generated media outside the coverage claim. +- [x] Keep Fastly's large-media deadline refusal in force. +- [x] Complete the full current repository gate set: all 12 `AGENTS.md` gates + passed on 2026-10-27 after the deadline and selector repairs, including + all eight target-matched clippy aliases, five adapter test aliases, + cross-adapter parity, JS, formatting and operator-script checks. +- [ ] Establish public detached-manifest retrieval and deployed Image Optimizer + behavior before claiming either deployment property. + +## Review log + +- 2026-10-27 (GPT-6.1-Sol): reconciled the design, operator contract and + recorded verification with the implemented routed subset. No tests or + runtime checks were executed during this documentation pass. The existing + spec contained no historical review log; this entry does not establish the + two independent agent review rounds required by `CONTRIBUTING.md`. + +- Round 1 (2026-10-27, independent GPT-5.5): checked the spec against adapter, + module, source-preservation, cache, privacy and routing code. Found namespace + resolution outside the large-media deadline, malformed selector admission, + inaccurate atomic-failure wording, incomplete configuration bounds and an + omitted existing-deployment rollout account. The documentation now records + partial success, defaults/bounds and automatic bounded-media activation. + Parent reproduced the deadline and selector defects with failing regressions. + Source repairs now share the remaining budget and validate selectors through + the matching parser. + +- Round 2 (2026-10-27, independent GPT-6 Astra): verified the repaired normal + large-media path, selector admission/fallback and corrected specification. + Found the same deadline and no-timer guards missing on origin `304` + revalidation. Parent reproduced all three added revalidation regressions + before repair. The revised path shares one remaining budget across namespace + and cache lookup, refuses before I/O without a timer or positive budget, and + preserves the original `304` on refusal. Independent reinspection found no + remaining material premerge defect in that review scope. Parent then passed + all 27 large-media tests, including warm-cache and origin-manifest behavior. + The reviewers did not run checks; the final repository gate receipt is + tracked separately above. diff --git a/fastly.toml b/fastly.toml index 020b15ea28..bc8f06a7df 100644 --- a/fastly.toml +++ b/fastly.toml @@ -23,6 +23,12 @@ build = """ [local_server.backends] [local_server.kv_stores] + # The runtime KV store, linked as the logical `trusted_server_kv` that + # edgezero.toml declares. + [[local_server.kv_stores.trusted_server_kv]] + key = "placeholder" + data = "placeholder" + [[local_server.kv_stores.counter_store]] key = "placeholder" data = "placeholder" diff --git a/trusted-server.example.toml b/trusted-server.example.toml index 7765fa8871..3b87f7bd61 100644 --- a/trusted-server.example.toml +++ b/trusted-server.example.toml @@ -436,6 +436,7 @@ pull_sync_concurrency = 3 # "ad-tag.google.loader", # "ad-tag.google.diagnostics", # "headers.geo", +# "provenance.encypher", # "51degrees.client_hints", # "service_version", # ] @@ -930,6 +931,47 @@ auction_timeout_ms = 500 # [middleware.51degrees.client_hints] # critical = true +# Encypher text provenance. Signs the article on each page a [[serve]] entry +# names `provenance.encypher` for, with invisible markers and a C2PA manifest +# link, and passes every page it cannot sign through unchanged. Give the +# entry a `path` for each section of public articles to sign. The article's +# text and address go to the API; a page whose origin set a cookie or forbade +# shared caching is never sent, so mark every personalized response private +# at the origin. `worker_token` is the name of the secret-store entry holding +# the `cwk_` worker credential. Plans are cached in the runtime KV store; +# without one, pages pass through. +# [provenance] +# module = "encypher" +# +# [provenance.encypher] +# api_base_url = "https://api.example.com" +# domain = "publisher.example.com" +# worker_token = "encypher_worker_token" +# +# Optional background preparation, instead of signing on reader delivery. +# Leave this unset for the existing synchronous mode. Async mode never +# falls back to synchronous signing: a missing result stays unsigned. +# Run a publisher-owned native worker using +# crates/trusted-server-preparation-worker/worker.example.toml. +# execution = "async_prepared" +# +# [provenance.encypher.preparation] +# worker_url = "https://prep.publisher.example.com" +# loopback_url = "https://publisher.example.com" +# deployment_id = "publisher-preparation" +# public_host = "publisher.example.com" +# signal_secret = "encypher_preparation_signal" +# publication_secret = "encypher_preparation_publication" +# routes = ["/news/", "/media/"] +# reader_trigger = false +# +# Register publisher URLs with `ts preparation warm --register`, or put +# them in the worker's inventory. Publish/update jobs can warm changed URLs. +# Inventory reconciliation is independent of reader traffic; no eight-page +# limit applies, but worker capacity is bounded. This does not crawl origins. +# Set reader_trigger = true only to add best-effort hints on cache misses. +# The two secret-store values must match the worker's corresponding scopes. + # Permutive DMP. `organization_id` and `workspace_id` are required. # [audience] # module = "permutive"