diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e550a08 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Keep the conformance corpus byte-for-byte reproducible across platforms. +# The corpus is hash-bound and signature-verified against exact bytes, so its +# line endings must stay LF regardless of the checkout platform. +src/test/resources/corpus/**/*.json text eol=lf +src/test/resources/corpus/**/*.md text eol=lf +src/test/resources/corpus/**/*.py text eol=lf +src/test/resources/corpus/tools/bip39_english.txt text eol=lf + +# Source files: stable LF throughout the repo. +*.java text eol=lf +*.xml text eol=lf +*.md text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3ccb023 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + build: + name: Build and test (JDK 21) + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: maven + + - name: Build and run unit tests + run: mvn -B test + + - name: Verify implementation against the corpus (conformance) + # Runs the conformance suite that drives all 62 corpus vectors through + # the validation pipeline and asserts each verdict, diagnostic code, and + # structured details against corpus.json. This is the code-vs-corpus + # check: the build fails if the implementation diverges from any vector. + run: mvn -B test -Dtest=ConformanceTest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac19d16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +*.class +.claude/ diff --git a/README.md b/README.md index db07d9a..a0f2ed1 100644 --- a/README.md +++ b/README.md @@ -1 +1,98 @@ # entangled-api-java + +An independent Java reference implementation of the **Entangled v1.0** protocol, +built solely from the specification at +[`samjanny/entangled`](https://github.com/samjanny/entangled) tag `v1.0-rc.27` +(its `specs/`, `docs/`, and `corpus/`). + +## Why this exists + +This is a *second, isolated reading* of the Entangled specification. The existing +Rust implementation shares an author with the spec, so its conformance does not, +by itself, show that the spec reads unambiguously. This implementation was +written from scratch, by a different reader, **without reference to any other +implementation** of the protocol -- only the specification text and the +conformance corpus. Where the two implementations diverge, that divergence is a +signal about the spec, which is the point of the exercise. + +## Status + +Passes the full conformance corpus: **62 / 62 vectors** match the recorded +verdict, diagnostic code, and structured `details` byte-identically. + +> Note on vector count: the corpus at `v1.0-rc.27` contains **62** vectors +> (`corpus.json` `rc_target: 1.0-rc.27`). Some older release notes refer to "60 +> vectors"; the additional vectors are the rc.25-rc.27 additions (the +> manifest-updated future-skew, the runtime-pubkey resurrection, and the +> migration trio). This implementation targets the rc.27 corpus. + +## Building and testing + +Requires JDK 21 and Maven. The conformance corpus is checked in under +`src/test/resources/corpus` and is read as raw bytes (no normalization). + +```sh +export JAVA_HOME=/path/to/jdk-21 +mvn test # all unit tests + the 62-vector conformance suite +mvn test -Dtest=ConformanceTest # the code-vs-corpus conformance suite only +``` + +CI (`.github/workflows/ci.yml`) runs both on every push. + +## Design notes + +- **No third-party crypto.** Ed25519 verification, JCS canonicalization, + base64url, SHA, BIP-39 PIP derivation, and Tor v3 address decoding are all + implemented in-tree for byte-level control. The JDK's built-in Ed25519 + (`SunEC`) does not implement the strict `verify_strict` profile section 05 + mandates (small-order rejection for both `A` and `R`, canonical `R`, `S < L`, + cofactorless equation), so verification is hand-implemented over + `BigInteger` field arithmetic. +- **First-failing-stage precedence** (section 10) is enforced by running the + 10-stage pipeline in order and converting the first stage's rejection into the + verdict. +- **The integer grammar** (section 04) is validated as a whole-document Stage 5 + pre-pass, before closed-schema field-presence checks, to honor the spec's + requirement that numeric tokens are validated "before any conversion"; corpus + vector 140 fixes this ordering. +- **The Stage 2 byte cap** is selected by the expected document kind from the + fetch context (a real client knows whether it fetched `/manifest.json`, a + content path, or a submit response), since the kind-specific cap is enforced + before parsing. + +## Ambiguities found + +Per the spec's ambiguity protocol, genuine ambiguities encountered at the Java +boundary -- points where two conforming implementations could diverge with no +clear non-conformance, and which no corpus vector constrains -- were filed as +issues against `samjanny/entangled`: + +- **AMB-10** (issue #11): the diagnostic for a bad `origin.carrier` value + (e.g. `"i2p"`) is not pinned -- `E_SCHEMA_FIELD_SYNTAX` vs + `E_SCHEMA_ENUM_VIOLATION`. This implementation chose `E_SCHEMA_ENUM_VIOLATION`. +- **AMB-11** (issue #12): the stage/code for an uppercase or otherwise + non-canonical `origin.address` is not pinned -- Stage 5 + `E_SCHEMA_FIELD_SYNTAX` vs Stage 9 `E_BIND_ORIGIN`. This implementation chose + Stage 5 `E_SCHEMA_FIELD_SYNTAX`. + +Each chosen reading is documented in a code comment citing the spec passages +that motivated it. + +## Layout + +``` +src/main/java/org/entangled/ + DiagnosticCode, Diagnostic, Verdict, RejectException normative codes and outcomes + json/ strict JSON lexer/parser, JCS canonicalization + crypto/ strict Ed25519, base64url, SHA, BIP-39 PIP, Tor v3 address + schema/ closed-schema field/block/document validators (Stage 5) + pipeline/ the 10-stage validation pipeline and per-stage logic +src/test/java/org/entangled/ + ConformanceTest drives all 62 corpus vectors + unit tests for the JSON, JCS, crypto, and schema layers +src/test/resources/corpus/ the spec conformance corpus, verbatim +``` + +## License + +Follows the licensing of the upstream specification corpus it is built against. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..deea6eb --- /dev/null +++ b/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + org.entangled + entangled-api-java + 0.1.0 + jar + + entangled-api-java + Independent Java reference implementation of the Entangled v1.0 protocol, built from the specification. + + + UTF-8 + 21 + 5.10.2 + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/src/main/java/org/entangled/Diagnostic.java b/src/main/java/org/entangled/Diagnostic.java new file mode 100644 index 0000000..33f8327 --- /dev/null +++ b/src/main/java/org/entangled/Diagnostic.java @@ -0,0 +1,63 @@ +package org.entangled; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * A single diagnostic: a normative {@link DiagnosticCode} plus an optional + * structured {@code details} map (section 11 "Structured diagnostic format"). + * + *

The {@code details} map mirrors the structured {@code details} object the + * spec defines per code (for example {@code component}/{@code declared_bytes}/ + * {@code budget_bytes} for {@code E_SUBMIT_BUDGET}). Conformance vectors that + * carry {@code diagnostic_details} are matched against this map exactly. + */ +public final class Diagnostic { + + private final DiagnosticCode code; + private final Map details; + + private Diagnostic(DiagnosticCode code, Map details) { + this.code = code; + this.details = Collections.unmodifiableMap(details); + } + + public static Diagnostic of(DiagnosticCode code) { + return new Diagnostic(code, new LinkedHashMap<>()); + } + + public static Diagnostic of(DiagnosticCode code, Map details) { + return new Diagnostic(code, new LinkedHashMap<>(details)); + } + + public DiagnosticCode code() { + return code; + } + + public Map details() { + return details; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Diagnostic other)) { + return false; + } + return code == other.code && details.equals(other.details); + } + + @Override + public int hashCode() { + return Objects.hash(code, details); + } + + @Override + public String toString() { + return details.isEmpty() ? code.name() : code.name() + " " + details; + } +} diff --git a/src/main/java/org/entangled/DiagnosticCode.java b/src/main/java/org/entangled/DiagnosticCode.java new file mode 100644 index 0000000..13e0514 --- /dev/null +++ b/src/main/java/org/entangled/DiagnosticCode.java @@ -0,0 +1,139 @@ +package org.entangled; + +/** + * The normative diagnostic codes from specification section 11. + * + *

Each constant carries its normative {@code severity} and the pipeline + * {@code stage} (1 through 10) at which it is detected, as defined in section 11 + * and section 10. Codes that do not map to a pipeline stage use {@code stage 0}. + * + *

Only the codes reachable by this implementation are enumerated here; the + * full catalog is larger (transport, image, and several state/historical codes + * are not wire-constructible within the corpus scope, per corpus/README.md). + * Codes that are part of the catalog but not yet exercised are still listed so + * the enum mirrors section 11 faithfully. + */ +public enum DiagnosticCode { + + // --- Transport diagnostics (Stage 1) --- + E_TRANSPORT_STATUS(Severity.ERROR, 1), + E_TRANSPORT_REDIRECT(Severity.ERROR, 1), + E_TRANSPORT_CONTENT_TYPE(Severity.ERROR, 1), + E_TRANSPORT_CONTENT_LENGTH(Severity.ERROR, 1), + E_TRANSPORT_BODY_FAILURE(Severity.ERROR, 1), + E_TRANSPORT_RATE_LIMITED(Severity.ERROR, 1), + E_TRANSPORT_NOT_FOUND(Severity.ERROR, 1), + E_TRANSPORT_METHOD_NOT_ALLOWED(Severity.ERROR, 1), + E_TRANSPORT_PAYLOAD_TOO_LARGE(Severity.ERROR, 1), + E_TRANSPORT_UNAVAILABLE(Severity.ERROR, 1), + E_TRANSPORT_BAD_REQUEST(Severity.ERROR, 1), + E_TRANSPORT_CONTENT_ENCODING(Severity.ERROR, 1), + E_TRANSPORT_TRANSFER_ENCODING(Severity.ERROR, 1), + + // --- Input diagnostics (Stage 2) --- + E_INPUT_BYTE_CAP(Severity.ERROR, 2), + E_INPUT_UTF8(Severity.ERROR, 2), + E_INPUT_BOM(Severity.ERROR, 2), + + // --- Parsing diagnostics (Stage 3) --- + E_PARSE_JSON(Severity.ERROR, 3), + E_PARSE_NESTING_DEPTH(Severity.ERROR, 3), + E_PARSE_STRING_LENGTH(Severity.ERROR, 3), + E_PARSE_ARRAY_LENGTH(Severity.ERROR, 3), + E_PARSE_OBJECT_KEYS(Severity.ERROR, 3), + E_PARSE_DUPLICATE_KEY(Severity.ERROR, 3), + + // --- Document kind diagnostics (Stage 4) --- + E_KIND_MISSING_FIELDS(Severity.ERROR, 4), + E_KIND_SPEC_VERSION(Severity.ERROR, 4), + E_KIND_UNKNOWN(Severity.ERROR, 4), + + // --- Schema diagnostics (Stage 5) --- + E_SCHEMA_REQUIRED_FIELD(Severity.ERROR, 5), + E_SCHEMA_UNKNOWN_FIELD(Severity.ERROR, 5), + E_SCHEMA_BLOCK_NOT_PERMITTED(Severity.ERROR, 5), + E_SCHEMA_FIELD_TYPE(Severity.ERROR, 5), + E_SCHEMA_FIELD_RANGE(Severity.ERROR, 5), + E_SCHEMA_FIELD_SYNTAX(Severity.ERROR, 5), + E_SCHEMA_ENUM_VIOLATION(Severity.ERROR, 5), + E_SCHEMA_DUPLICATE_ENTRY(Severity.ERROR, 5), + E_SCHEMA_FIELD_LENGTH(Severity.ERROR, 5), + E_SCHEMA_NULL_VALUE(Severity.ERROR, 5), + E_SCHEMA_NON_INTEGER(Severity.ERROR, 5), + E_SCHEMA_MALFORMED_UNICODE(Severity.ERROR, 5), + E_SUBMIT_BUDGET(Severity.ERROR, 5), + E_ORIGIN_INVALID(Severity.ERROR, 5), + + // --- Signature diagnostics (Stage 6) --- + E_SIG_VERIFICATION(Severity.ERROR, 6), + E_SIG_INVALID_KEY(Severity.ERROR, 6), + E_SIG_MALFORMED(Severity.ERROR, 6), + + // --- Trust state diagnostics (Stage 6 pre-check and Stage 7) --- + E_TRUST_MISMATCH(Severity.ERROR, 6), + E_TRUST_USER_REJECTED(Severity.ERROR, 6), + I_TRUST_FIRST_CONTACT(Severity.INFO, 7), + I_TRUST_TOFU_PINNED(Severity.INFO, 7), + I_TRUST_VERIFIED(Severity.INFO, 7), + + // --- Canary diagnostics (Stage 8) --- + E_CANARY_INVALID(Severity.ERROR, 8), + E_CANARY_DOWNGRADE(Severity.ERROR, 8), + E_CANARY_CONFLICT(Severity.ERROR, 8), + W_CANARY_NEAR_EXPIRATION(Severity.WARNING, 8), + E_CANARY_EXPIRED(Severity.ERROR, 8), + W_CANARY_GAP(Severity.WARNING, 8), + W_CANARY_UNAVAILABLE(Severity.WARNING, 8), + E_CANARY_RUNTIME_REUSE(Severity.ERROR, 8), + + // --- Binding diagnostics (Stage 9) --- + E_BIND_PATH(Severity.ERROR, 9), + E_BIND_RESPONSE_PATH(Severity.ERROR, 9), + E_BIND_REQUEST_ID(Severity.ERROR, 9), + E_BIND_REQUEST_HASH(Severity.ERROR, 9), + E_BIND_ORIGIN(Severity.ERROR, 9), + E_ORIGIN_EXPIRED(Severity.ERROR, 9), + E_MIGRATION_MISMATCH(Severity.ERROR, 9), + E_MIGRATION_INVALID(Severity.ERROR, 9), + E_CONTENT_INDEX_FETCH_FAILED(Severity.ERROR, 9), + E_CONTENT_INDEX_HASH_MISMATCH(Severity.ERROR, 9), + E_CONTENT_INDEX_INVALID(Severity.ERROR, 9), + E_CONTENT_SEQ_MISSING(Severity.ERROR, 9), + E_CONTENT_SEQ_ROLLBACK(Severity.ERROR, 9), + E_CONTENT_SEQ_UNCOMMITTED(Severity.ERROR, 9), + E_CONTENT_HASH_MISMATCH(Severity.ERROR, 9), + + // --- State diagnostics --- + E_STATE_UNDECLARED(Severity.ERROR, 0), + E_STATE_VALUE_SIZE(Severity.ERROR, 0), + E_STATE_TTL(Severity.ERROR, 0), + E_STATE_OP(Severity.ERROR, 0), + E_STATE_STORAGE_CAP(Severity.ERROR, 0), + E_STATE_TRANSMIT_BUDGET(Severity.ERROR, 0), + E_STATE_DUPLICATE(Severity.ERROR, 0), + + // --- Historical content diagnostics --- + E_HISTORICAL_NO_AUTHORIZATION(Severity.ERROR, 0), + E_HISTORICAL_NO_PUBLICATION_PROOF(Severity.ERROR, 0), + E_HISTORICAL_TRUST_BLOCKED(Severity.ERROR, 0), + E_HISTORICAL_RUNTIME_AMBIGUOUS(Severity.ERROR, 0); + + /** Severity classes from section 11. */ + public enum Severity { ERROR, WARNING, INFO } + + private final Severity severity; + private final int stage; + + DiagnosticCode(Severity severity, int stage) { + this.severity = severity; + this.stage = stage; + } + + public Severity severity() { + return severity; + } + + public int stage() { + return stage; + } +} diff --git a/src/main/java/org/entangled/Entangled.java b/src/main/java/org/entangled/Entangled.java new file mode 100644 index 0000000..0f9c723 --- /dev/null +++ b/src/main/java/org/entangled/Entangled.java @@ -0,0 +1,23 @@ +package org.entangled; + +/** + * Entangled v1.0 reference implementation (Java). + * + *

This is an independent implementation built solely from the Entangled + * specification at samjanny/entangled tag v1.0-rc.27 (specs/, docs/, corpus/). + * It was written without reference to any other implementation of the protocol. + * + *

The protocol version targeted on the wire is exactly "1.0"; the spec + * revision this code was read against is recorded in {@link #SPEC_REVISION}. + */ +public final class Entangled { + + /** The on-the-wire protocol version every document must declare (section 02, section 11). */ + public static final String SPEC_VERSION = "1.0"; + + /** The spec revision this implementation was read against. */ + public static final String SPEC_REVISION = "1.0-rc.27"; + + private Entangled() { + } +} diff --git a/src/main/java/org/entangled/RejectException.java b/src/main/java/org/entangled/RejectException.java new file mode 100644 index 0000000..8de1cbb --- /dev/null +++ b/src/main/java/org/entangled/RejectException.java @@ -0,0 +1,33 @@ +package org.entangled; + +import java.util.Map; + +/** + * Internal control-flow exception used by the validation pipeline to abort at + * the first failing stage (section 10 error precedence). It carries the single + * {@link Diagnostic} that becomes the rejection verdict. + * + *

This is never surfaced to callers; the pipeline catches it and converts it + * to a {@link Verdict#reject}. + */ +public final class RejectException extends RuntimeException { + + private final transient Diagnostic diagnostic; + + public RejectException(Diagnostic diagnostic) { + super(diagnostic.code().name()); + this.diagnostic = diagnostic; + } + + public RejectException(DiagnosticCode code) { + this(Diagnostic.of(code)); + } + + public RejectException(DiagnosticCode code, Map details) { + this(Diagnostic.of(code, details)); + } + + public Diagnostic diagnostic() { + return diagnostic; + } +} diff --git a/src/main/java/org/entangled/Verdict.java b/src/main/java/org/entangled/Verdict.java new file mode 100644 index 0000000..4b84d10 --- /dev/null +++ b/src/main/java/org/entangled/Verdict.java @@ -0,0 +1,51 @@ +package org.entangled; + +import java.util.Map; + +/** + * The outcome of running a document (or scenario) through the validation + * pipeline: either {@code accept}, or {@code reject} with a single + * {@link Diagnostic}. + * + *

Per section 10 error precedence, a rejection reports the first failing + * stage; this type therefore carries exactly one diagnostic. + */ +public final class Verdict { + + private final boolean accepted; + private final Diagnostic diagnostic; + + private Verdict(boolean accepted, Diagnostic diagnostic) { + this.accepted = accepted; + this.diagnostic = diagnostic; + } + + public static Verdict accept() { + return new Verdict(true, null); + } + + public static Verdict reject(Diagnostic diagnostic) { + return new Verdict(false, diagnostic); + } + + public static Verdict reject(DiagnosticCode code) { + return reject(Diagnostic.of(code)); + } + + public static Verdict reject(DiagnosticCode code, Map details) { + return reject(Diagnostic.of(code, details)); + } + + public boolean isAccepted() { + return accepted; + } + + public Diagnostic diagnostic() { + return diagnostic; + } + + @Override + public String toString() { + return accepted ? "ACCEPT" : "REJECT(" + diagnostic + ")"; + } +} diff --git a/src/main/java/org/entangled/crypto/Base64Url.java b/src/main/java/org/entangled/crypto/Base64Url.java new file mode 100644 index 0000000..42f88e3 --- /dev/null +++ b/src/main/java/org/entangled/crypto/Base64Url.java @@ -0,0 +1,95 @@ +package org.entangled.crypto; + +/** + * Strict base64url decoding per RFC 4648 Section 5 ("Base 64 Encoding with URL + * and Filename Safe Alphabet"), with the additional strictness Entangled + * requires (section 04 "Strict base64url decoding"). + * + *

The decoder: + *

+ * + *

A violation of any rule is signalled by {@link InvalidBase64Url}; callers + * map it to {@code E_SCHEMA_FIELD_SYNTAX} at Stage 5. + */ +public final class Base64Url { + + /** Thrown when a base64url field violates the strict decoding rules. */ + public static final class InvalidBase64Url extends RuntimeException { + public InvalidBase64Url(String message) { + super(message); + } + } + + private static final int[] DECODE = new int[128]; + + static { + for (int i = 0; i < DECODE.length; i++) { + DECODE[i] = -1; + } + String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + for (int i = 0; i < alphabet.length(); i++) { + DECODE[alphabet.charAt(i)] = i; + } + } + + private Base64Url() { + } + + /** + * Decode an unpadded base64url string into exactly {@code expectedBytes} + * bytes. The encoded length must equal {@code ceil(expectedBytes * 8 / 6)} + * and the input must satisfy every strictness rule above. + * + * @param s the encoded string + * @param expectedBytes the exact number of decoded bytes the field declares + * @return the decoded bytes (length {@code expectedBytes}) + */ + public static byte[] decode(String s, int expectedBytes) { + int expectedChars = (expectedBytes * 8 + 5) / 6; + if (s.length() != expectedChars) { + throw new InvalidBase64Url("length " + s.length() + " != expected " + expectedChars); + } + byte[] out = new byte[expectedBytes]; + int outPos = 0; + int buffer = 0; + int bits = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 128) { + throw new InvalidBase64Url("non-ASCII character at " + i); + } + int v = DECODE[c]; + if (v < 0) { + throw new InvalidBase64Url("character outside base64url alphabet at " + i + ": " + c); + } + buffer = (buffer << 6) | v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[outPos++] = (byte) ((buffer >> bits) & 0xFF); + } + } + // Canonical-encoding check: any leftover bits in the final group must be zero. + if (bits > 0) { + int mask = (1 << bits) - 1; + if ((buffer & mask) != 0) { + throw new InvalidBase64Url("non-canonical trailing bits"); + } + } + if (outPos != expectedBytes) { + // Should not happen given the length check, but guard anyway. + throw new InvalidBase64Url("decoded " + outPos + " bytes, expected " + expectedBytes); + } + return out; + } +} diff --git a/src/main/java/org/entangled/crypto/Bip39Pip.java b/src/main/java/org/entangled/crypto/Bip39Pip.java new file mode 100644 index 0000000..939a54f --- /dev/null +++ b/src/main/java/org/entangled/crypto/Bip39Pip.java @@ -0,0 +1,89 @@ +package org.entangled.crypto; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Publisher Identity Phrase (PIP) derivation, section 05. + * + *

The PIP is a 24-word public phrase derived from the raw 32-byte Ed25519 + * public key {@code K_publisher.pub} using the BIP-39 English wordlist and + * checksum procedure: + *

    + *
  1. {@code entropy = K_publisher.pub} (32 bytes);
  2. + *
  3. {@code checksum = first_8_bits(SHA-256(entropy))};
  4. + *
  5. {@code bits = entropy || checksum} (264 bits);
  6. + *
  7. split into 24 groups of 11 bits;
  8. + *
  9. each group indexes the BIP-39 English wordlist;
  10. + *
  11. join the 24 words with single ASCII spaces.
  12. + *
+ * + *

This is an encoding of a public key, not a wallet seed. The wordlist is the + * canonical BIP-39 English list, bundled as a resource. + */ +public final class Bip39Pip { + + private static final List WORDLIST = loadWordlist(); + + private Bip39Pip() { + } + + /** Derive the 24-word PIP from a 32-byte Ed25519 public key. */ + public static String derive(byte[] publisherPub) { + if (publisherPub.length != 32) { + throw new IllegalArgumentException("K_publisher.pub must be 32 bytes"); + } + byte[] checksum = Sha.sha256(publisherPub); + // 256 entropy bits + 8 checksum bits = 264 bits = 24 * 11. + byte[] bits = new byte[33]; + System.arraycopy(publisherPub, 0, bits, 0, 32); + bits[32] = checksum[0]; + + List words = new ArrayList<>(24); + for (int i = 0; i < 24; i++) { + int index = elevenBitsAt(bits, i * 11); + words.add(WORDLIST.get(index)); + } + return String.join(" ", words); + } + + /** Extract the 11-bit big-endian group starting at bit offset {@code bitOffset}. */ + private static int elevenBitsAt(byte[] bits, int bitOffset) { + int value = 0; + for (int i = 0; i < 11; i++) { + int bitIndex = bitOffset + i; + int b = (bits[bitIndex >> 3] >> (7 - (bitIndex & 7))) & 1; + value = (value << 1) | b; + } + return value; + } + + private static List loadWordlist() { + List words = new ArrayList<>(2048); + try (InputStream in = Bip39Pip.class.getResourceAsStream("bip39_english.txt")) { + if (in == null) { + throw new IllegalStateException("bip39_english.txt resource missing"); + } + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + String w = line.strip(); + if (!w.isEmpty()) { + words.add(w); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + if (words.size() != 2048) { + throw new IllegalStateException("BIP-39 wordlist must have 2048 words, got " + words.size()); + } + return words; + } +} diff --git a/src/main/java/org/entangled/crypto/Ed25519.java b/src/main/java/org/entangled/crypto/Ed25519.java new file mode 100644 index 0000000..02d06aa --- /dev/null +++ b/src/main/java/org/entangled/crypto/Ed25519.java @@ -0,0 +1,205 @@ +package org.entangled.crypto; + +import java.math.BigInteger; +import java.util.Arrays; + +/** + * Strict Ed25519 verification, RFC 8032 plus the additional rejections required + * by section 05 ("Ed25519 verification profile"). + * + *

A signature is accepted only if: + *

+ * + *

This is the {@code verify_strict} profile section 05 names: small-order + * rejection applies symmetrically to {@code A} and {@code R}, non-canonical + * encodings of {@code A}/{@code R} are rejected, non-canonical {@code S} + * ({@code S >= L}) is rejected, and verification is cofactorless (never the + * cofactored {@code [8S]B = [8]R + [8][k]A} form). Pure-Java arithmetic with + * {@link BigInteger} is used for unambiguous, dependency-free byte-level control; + * the corpus is small so performance is not a concern. + */ +public final class Ed25519 { + + // Static initializers run in textual order, so each constant is declared + // after the constants its initializer depends on. + private static final BigInteger TWO = BigInteger.TWO; + private static final BigInteger P = + TWO.pow(255).subtract(BigInteger.valueOf(19)); + // Group order L = 2^252 + 27742317777372353535851937790883648493. + private static final BigInteger L = + TWO.pow(252).add(new BigInteger("27742317777372353535851937790883648493")); + private static final BigInteger D = + BigInteger.valueOf(-121665).multiply(inverse(BigInteger.valueOf(121666))).mod(P); + + // sqrt(-1) mod p, used in point decompression. + private static final BigInteger I = TWO.modPow(P.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P); + + // Base point B (depends on D and I, so declared after them). + private static final Point B = basePoint(); + + private Ed25519() { + } + + /** + * Verify a signature under the strict profile. + * + * @param publicKey 32-byte compressed public key A + * @param signature 64-byte signature R || S + * @param message the signature input M (context || 0x00 || JCS(payload)) + * @return true iff the signature is valid under the strict profile + */ + public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) { + if (publicKey.length != 32 || signature.length != 64) { + return false; + } + // Decode A; reject non-canonical encoding and small-order points. + Point a = decodePoint(publicKey); + if (a == null || isSmallOrder(a)) { + return false; + } + byte[] rBytes = Arrays.copyOfRange(signature, 0, 32); + byte[] sBytes = Arrays.copyOfRange(signature, 32, 64); + + // Decode R; reject non-canonical encoding and small-order points. + Point r = decodePoint(rBytes); + if (r == null || isSmallOrder(r)) { + return false; + } + // S must be canonical: 0 <= S < L. + BigInteger s = leToBigInteger(sBytes); + if (s.compareTo(L) >= 0) { + return false; + } + // k = SHA-512(R || A || M) mod L. + byte[] kInput = concat(rBytes, publicKey, message); + BigInteger k = leToBigInteger(Sha.sha512(kInput)).mod(L); + + // Cofactorless check: [S]B == R + [k]A. + Point left = scalarMul(B, s); + Point right = edwardsAdd(r, scalarMul(a, k)); + return pointEquals(left, right); + } + + // --- Point model (affine, BigInteger coordinates mod p) --- + + private record Point(BigInteger x, BigInteger y) { + } + + private static Point basePoint() { + BigInteger by = BigInteger.valueOf(4).multiply(inverse(BigInteger.valueOf(5))).mod(P); + BigInteger bx = recoverX(by, false); + return new Point(bx, by); + } + + private static boolean pointEquals(Point a, Point b) { + return a.x.equals(b.x) && a.y.equals(b.y); + } + + private static Point edwardsAdd(Point p1, Point p2) { + BigInteger x1 = p1.x; + BigInteger y1 = p1.y; + BigInteger x2 = p2.x; + BigInteger y2 = p2.y; + BigInteger dxy = D.multiply(x1).multiply(x2).multiply(y1).multiply(y2).mod(P); + BigInteger xNum = x1.multiply(y2).add(x2.multiply(y1)).mod(P); + BigInteger xDen = BigInteger.ONE.add(dxy).mod(P); + BigInteger yNum = y1.multiply(y2).add(x1.multiply(x2)).mod(P); + BigInteger yDen = BigInteger.ONE.subtract(dxy).mod(P); + BigInteger x3 = xNum.multiply(inverse(xDen)).mod(P); + BigInteger y3 = yNum.multiply(inverse(yDen)).mod(P); + return new Point(x3, y3); + } + + private static Point scalarMul(Point point, BigInteger e) { + Point result = new Point(BigInteger.ZERO, BigInteger.ONE); // neutral element + Point base = point; + BigInteger k = e; + while (k.signum() > 0) { + if (k.testBit(0)) { + result = edwardsAdd(result, base); + } + base = edwardsAdd(base, base); + k = k.shiftRight(1); + } + return result; + } + + /** + * A point is small-order if multiplying it by the cofactor 8 yields the + * neutral element. RFC 8032 strict verification rejects such points for both + * A and R (section 05). + */ + private static boolean isSmallOrder(Point p) { + Point eightP = scalarMul(p, BigInteger.valueOf(8)); + return eightP.x.signum() == 0 && eightP.y.equals(BigInteger.ONE); + } + + /** + * Decode a 32-byte compressed point. Returns null if the encoding is + * non-canonical or does not decode to a curve point. The sign bit is the top + * bit of the last byte; the remaining 255 bits are y, which must be < p + * (canonical encoding). + */ + private static Point decodePoint(byte[] enc) { + BigInteger y = leToBigInteger(enc); + BigInteger signBit = y.shiftRight(255).and(BigInteger.ONE); + y = y.and(TWO.pow(255).subtract(BigInteger.ONE)); + // Canonical encoding requires y < p. + if (y.compareTo(P) >= 0) { + return null; + } + BigInteger x = recoverX(y, signBit.testBit(0)); + if (x == null) { + return null; + } + return new Point(x, y); + } + + /** Recover x from y and the sign bit on the Edwards curve, or null if none exists. */ + private static BigInteger recoverX(BigInteger y, boolean xIsOdd) { + BigInteger y2 = y.multiply(y).mod(P); + BigInteger num = y2.subtract(BigInteger.ONE).mod(P); + BigInteger den = D.multiply(y2).add(BigInteger.ONE).mod(P); + BigInteger xx = num.multiply(inverse(den)).mod(P); + BigInteger x = xx.modPow(P.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P); + if (!x.multiply(x).subtract(xx).mod(P).equals(BigInteger.ZERO)) { + x = x.multiply(I).mod(P); + } + if (!x.multiply(x).subtract(xx).mod(P).equals(BigInteger.ZERO)) { + return null; // no square root: not a valid point + } + if (x.testBit(0) != xIsOdd) { + x = P.subtract(x); + } + return x; + } + + private static BigInteger inverse(BigInteger a) { + return a.modPow(P.subtract(TWO), P); + } + + private static BigInteger leToBigInteger(byte[] le) { + byte[] be = new byte[le.length]; + for (int i = 0; i < le.length; i++) { + be[i] = le[le.length - 1 - i]; + } + return new BigInteger(1, be); + } + + private static byte[] concat(byte[] a, byte[] b, byte[] c) { + byte[] out = new byte[a.length + b.length + c.length]; + System.arraycopy(a, 0, out, 0, a.length); + System.arraycopy(b, 0, out, a.length, b.length); + System.arraycopy(c, 0, out, a.length + b.length, c.length); + return out; + } +} diff --git a/src/main/java/org/entangled/crypto/Sha.java b/src/main/java/org/entangled/crypto/Sha.java new file mode 100644 index 0000000..c9718a1 --- /dev/null +++ b/src/main/java/org/entangled/crypto/Sha.java @@ -0,0 +1,34 @@ +package org.entangled.crypto; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Thin wrappers over the JDK {@link MessageDigest} for SHA-256 and SHA-512. + * + *

SHA-256 is used for the PIP checksum (section 05), image and content hash + * binding, and {@code request_hash} (section 02, section 03). SHA-512 is used + * inside the Ed25519 verification equation (RFC 8032). These are standard, + * unambiguous primitives, so the JDK implementations are used directly. + */ +public final class Sha { + + private Sha() { + } + + public static byte[] sha256(byte[] data) { + return digest("SHA-256", data); + } + + public static byte[] sha512(byte[] data) { + return digest("SHA-512", data); + } + + private static byte[] digest(String algorithm, byte[] data) { + try { + return MessageDigest.getInstance(algorithm).digest(data); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(algorithm + " unavailable", e); + } + } +} diff --git a/src/main/java/org/entangled/crypto/TorV3Address.java b/src/main/java/org/entangled/crypto/TorV3Address.java new file mode 100644 index 0000000..6bc6186 --- /dev/null +++ b/src/main/java/org/entangled/crypto/TorV3Address.java @@ -0,0 +1,113 @@ +package org.entangled.crypto; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +/** + * Tor v3 onion address decoding, following the rend-spec-v3 "Encoding onion + * addresses" procedure referenced by section 05. + * + *

A v3 address is {@code base32(PUBKEY || CHECKSUM || VERSION) + ".onion"}, + * where {@code PUBKEY} is the 32-byte Ed25519 service public key, + * {@code CHECKSUM = SHA3-256(".onion checksum" || PUBKEY || VERSION)[:2]}, and + * {@code VERSION = 0x03}. The base32 alphabet is lowercase RFC 4648 ({@code a-z2-7}), + * yielding a 56-character address body before the {@code .onion} suffix. + * + *

Decoding validates the structure, the version byte, and the checksum, and + * returns the embedded public key so the caller can compare it to + * {@code origin.origin_pubkey} (origin binding, section 06; failure maps to + * {@code E_BIND_ORIGIN}). + */ +public final class TorV3Address { + + /** Thrown when an address is not a structurally valid Tor v3 onion address. */ + public static final class InvalidOnionAddress extends RuntimeException { + public InvalidOnionAddress(String message) { + super(message); + } + } + + private static final String SUFFIX = ".onion"; + private static final int ADDRESS_BODY_LEN = 56; + private static final byte VERSION = 0x03; + private static final byte[] CHECKSUM_PREFIX = ".onion checksum".getBytes(java.nio.charset.StandardCharsets.US_ASCII); + private static final String BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567"; + + private TorV3Address() { + } + + /** + * Decode and validate a canonical Tor v3 address; return the 32-byte service + * public key. The address must be lowercase, exactly 56 base32 characters + * before the {@code .onion} suffix, with no scheme, port, path, query, or + * fragment. + */ + public static byte[] decodePublicKey(String address) { + if (!address.endsWith(SUFFIX)) { + throw new InvalidOnionAddress("missing .onion suffix"); + } + String body = address.substring(0, address.length() - SUFFIX.length()); + if (body.length() != ADDRESS_BODY_LEN) { + throw new InvalidOnionAddress("address body must be 56 characters, got " + body.length()); + } + byte[] decoded = base32Decode(body); // 35 bytes + if (decoded.length != 35) { + throw new InvalidOnionAddress("decoded length must be 35 bytes"); + } + byte[] pubkey = Arrays.copyOfRange(decoded, 0, 32); + byte[] checksum = Arrays.copyOfRange(decoded, 32, 34); + byte version = decoded[34]; + if (version != VERSION) { + throw new InvalidOnionAddress("version byte must be 0x03"); + } + byte[] expectedChecksum = computeChecksum(pubkey, version); + if (!(checksum[0] == expectedChecksum[0] && checksum[1] == expectedChecksum[1])) { + throw new InvalidOnionAddress("checksum mismatch"); + } + return pubkey; + } + + private static byte[] computeChecksum(byte[] pubkey, byte version) { + byte[] input = new byte[CHECKSUM_PREFIX.length + pubkey.length + 1]; + int pos = 0; + System.arraycopy(CHECKSUM_PREFIX, 0, input, pos, CHECKSUM_PREFIX.length); + pos += CHECKSUM_PREFIX.length; + System.arraycopy(pubkey, 0, input, pos, pubkey.length); + pos += pubkey.length; + input[pos] = version; + byte[] hash = sha3_256(input); + return new byte[] {hash[0], hash[1]}; + } + + private static byte[] sha3_256(byte[] data) { + try { + return MessageDigest.getInstance("SHA3-256").digest(data); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA3-256 unavailable", e); + } + } + + /** Lowercase RFC 4648 base32 decode (no padding); 56 chars -> 35 bytes. */ + private static byte[] base32Decode(String s) { + int outLen = s.length() * 5 / 8; + byte[] out = new byte[outLen]; + int buffer = 0; + int bits = 0; + int outPos = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + int v = BASE32_ALPHABET.indexOf(c); + if (v < 0) { + throw new InvalidOnionAddress("character outside lowercase base32 alphabet: " + c); + } + buffer = (buffer << 5) | v; + bits += 5; + if (bits >= 8) { + bits -= 8; + out[outPos++] = (byte) ((buffer >> bits) & 0xFF); + } + } + return out; + } +} diff --git a/src/main/java/org/entangled/json/Jcs.java b/src/main/java/org/entangled/json/Jcs.java new file mode 100644 index 0000000..f04608e --- /dev/null +++ b/src/main/java/org/entangled/json/Jcs.java @@ -0,0 +1,151 @@ +package org.entangled.json; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * JSON Canonicalization Scheme (RFC 8785) over the restricted Entangled input + * space (section 04). + * + *

Within Entangled's grammar JCS reduces to four rules: + *

+ * + *

Entangled conforming documents contain no {@code null} and no + * floating-point numbers, so those branches are rejected at schema validation + * before canonicalization is ever invoked. Canonicalizing such a value here is a + * programming error and throws. + */ +public final class Jcs { + + private Jcs() { + } + + /** Canonicalize a value to the RFC 8785 byte sequence (UTF-8). */ + public static byte[] canonicalize(JsonValue value) { + StringBuilder sb = new StringBuilder(); + write(value, sb); + return sb.toString().getBytes(StandardCharsets.UTF_8); + } + + /** Canonicalize and return the UTF-8 string form (used by tests). */ + public static String canonicalString(JsonValue value) { + StringBuilder sb = new StringBuilder(); + write(value, sb); + return sb.toString(); + } + + private static void write(JsonValue value, StringBuilder sb) { + if (value instanceof JsonValue.Obj obj) { + writeObject(obj, sb); + } else if (value instanceof JsonValue.Arr arr) { + writeArray(arr, sb); + } else if (value instanceof JsonValue.Str str) { + writeString(str.value(), sb); + } else if (value instanceof JsonValue.Num num) { + writeNumber(num, sb); + } else if (value instanceof JsonValue.Bool b) { + sb.append(b.value() ? "true" : "false"); + } else { + // JsonValue.Null: not canonicalizable in a conforming document. + throw new IllegalStateException("null literal cannot be canonicalized (section 04)"); + } + } + + private static void writeObject(JsonValue.Obj obj, StringBuilder sb) { + List keys = new ArrayList<>(obj.members().keySet()); + // RFC 8785: sort by UTF-16 code-unit comparison of member names. + keys.sort(Jcs::compareUtf16CodeUnits); + sb.append('{'); + boolean first = true; + for (String key : keys) { + if (!first) { + sb.append(','); + } + first = false; + writeString(key, sb); + sb.append(':'); + write(obj.members().get(key), sb); + } + sb.append('}'); + } + + private static void writeArray(JsonValue.Arr arr, StringBuilder sb) { + sb.append('['); + boolean first = true; + for (JsonValue element : arr.elements()) { + if (!first) { + sb.append(','); + } + first = false; + write(element, sb); + } + sb.append(']'); + } + + private static void writeNumber(JsonValue.Num num, StringBuilder sb) { + if (!num.conformingInteger()) { + throw new IllegalStateException( + "non-integer number cannot be canonicalized (section 04): " + num.raw()); + } + // Exact decimal digits of the magnitude; the conforming-integer flag + // already guarantees no sign, leading zero, point, or exponent. + sb.append(num.value().toString()); + } + + /** + * Minimal JSON string escaping per RFC 8785. Java strings are UTF-16, which + * is the code-unit basis RFC 8785 uses for both escaping decisions and key + * ordering, so iterating by char is correct. + */ + static void writeString(String s, StringBuilder sb) { + sb.append('"'); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"' -> sb.append("\\\""); + case '\\' -> sb.append("\\\\"); + case '\b' -> sb.append("\\b"); + case '\t' -> sb.append("\\t"); + case '\n' -> sb.append("\\n"); + case '\f' -> sb.append("\\f"); + case '\r' -> sb.append("\\r"); + default -> { + if (c < 0x20) { + sb.append("\\u"); + sb.append(hex4(c)); + } else { + sb.append(c); + } + } + } + } + sb.append('"'); + } + + private static String hex4(int c) { + String h = Integer.toHexString(c); + return "0000".substring(h.length()) + h; + } + + private static int compareUtf16CodeUnits(String a, String b) { + // String.compareTo compares by UTF-16 code units, matching RFC 8785. + return a.compareTo(b); + } +} diff --git a/src/main/java/org/entangled/json/JsonParser.java b/src/main/java/org/entangled/json/JsonParser.java new file mode 100644 index 0000000..fb6198d --- /dev/null +++ b/src/main/java/org/entangled/json/JsonParser.java @@ -0,0 +1,384 @@ +package org.entangled.json; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; + +/** + * Strict JSON parser for Entangled Stage 3 (section 10, section 04). + * + *

The parser operates on a Java String that the caller has already validated + * as strict UTF-8 with no BOM (Stage 2). It enforces, at parse time, the + * normative parser limits (nesting depth 16, string length 100 KiB, array + * length 10000, object keys 256 per object) and rejects duplicate member names. + * Any structural malformation is reported as {@code E_PARSE_JSON}. + * + *

Number tokens are accepted as valid JSON and classified against the + * Entangled integer grammar (section 04); non-conforming numbers (floats, + * exponents, leading zeros, signs, out-of-range) are not rejected here but are + * flagged for Stage 5 ({@code E_SCHEMA_NON_INTEGER}). The {@code null} literal is + * parsed and rejected later at Stage 5 ({@code E_SCHEMA_NULL_VALUE}). + */ +public final class JsonParser { + + /** Section 10 parser limits. */ + static final int MAX_DEPTH = 16; + static final int MAX_STRING_LENGTH = 100 * 1024; + static final int MAX_ARRAY_LENGTH = 10000; + static final int MAX_OBJECT_KEYS = 256; + + private static final BigInteger MAX_INT63 = BigInteger.valueOf(Long.MAX_VALUE); // 2^63 - 1 + + private final String s; + private int pos; + + private JsonParser(String s) { + this.s = s; + } + + /** Parse the whole input as a single JSON value; trailing non-whitespace is an error. */ + public static JsonValue parse(String input) { + JsonParser p = new JsonParser(input); + p.skipWs(); + JsonValue v = p.parseValue(0); + p.skipWs(); + if (p.pos != p.s.length()) { + throw parseError(); + } + return v; + } + + private JsonValue parseValue(int depth) { + if (pos >= s.length()) { + throw parseError(); + } + char c = s.charAt(pos); + return switch (c) { + case '{' -> parseObject(depth); + case '[' -> parseArray(depth); + case '"' -> new JsonValue.Str(parseString()); + case 't', 'f' -> parseBool(); + case 'n' -> parseNull(); + default -> { + if (c == '-' || (c >= '0' && c <= '9')) { + yield parseNumber(); + } + throw parseError(); + } + }; + } + + private JsonValue parseObject(int depth) { + enterDepth(depth); + expect('{'); + Map members = new LinkedHashMap<>(); + skipWs(); + if (peek() == '}') { + pos++; + return new JsonValue.Obj(members); + } + while (true) { + skipWs(); + if (peek() != '"') { + throw parseError(); + } + String key = parseString(); + if (members.containsKey(key)) { + // Section 04: duplicate member names are rejected during parsing, + // not first-wins or last-wins. Reported as E_PARSE_DUPLICATE_KEY. + Map details = new LinkedHashMap<>(); + details.put("duplicate_key", key); + throw new RejectException(DiagnosticCode.E_PARSE_DUPLICATE_KEY, details); + } + skipWs(); + expect(':'); + skipWs(); + JsonValue value = parseValue(depth + 1); + members.put(key, value); + if (members.size() > MAX_OBJECT_KEYS) { + throw new RejectException(DiagnosticCode.E_PARSE_OBJECT_KEYS); + } + skipWs(); + char n = next(); + if (n == ',') { + continue; + } + if (n == '}') { + break; + } + throw parseError(); + } + return new JsonValue.Obj(members); + } + + private JsonValue parseArray(int depth) { + enterDepth(depth); + expect('['); + List elements = new ArrayList<>(); + skipWs(); + if (peek() == ']') { + pos++; + return new JsonValue.Arr(elements); + } + while (true) { + skipWs(); + JsonValue value = parseValue(depth + 1); + elements.add(value); + if (elements.size() > MAX_ARRAY_LENGTH) { + throw new RejectException(DiagnosticCode.E_PARSE_ARRAY_LENGTH); + } + skipWs(); + char n = next(); + if (n == ',') { + continue; + } + if (n == ']') { + break; + } + throw parseError(); + } + return new JsonValue.Arr(elements); + } + + private String parseString() { + expect('"'); + StringBuilder sb = new StringBuilder(); + while (true) { + if (pos >= s.length()) { + throw parseError(); + } + char c = s.charAt(pos++); + if (c == '"') { + break; + } + if (c == '\\') { + if (pos >= s.length()) { + throw parseError(); + } + char e = s.charAt(pos++); + switch (e) { + case '"' -> sb.append('"'); + case '\\' -> sb.append('\\'); + case '/' -> sb.append('/'); + case 'b' -> sb.append('\b'); + case 'f' -> sb.append('\f'); + case 'n' -> sb.append('\n'); + case 'r' -> sb.append('\r'); + case 't' -> sb.append('\t'); + case 'u' -> sb.append(parseUnicodeEscape()); + default -> throw parseError(); + } + } else if (c < 0x20) { + // Raw control characters are not permitted in JSON strings. + throw parseError(); + } else { + sb.append(c); + } + if (sb.length() > MAX_STRING_LENGTH) { + throw new RejectException(DiagnosticCode.E_PARSE_STRING_LENGTH); + } + } + return sb.toString(); + } + + /** + * Parse a {@code \\uXXXX} escape, joining a valid high/low surrogate pair. + * + *

Section 04 forbids isolated surrogate escapes and malformed Unicode + * escapes. An isolated or mispaired surrogate, or a non-hex escape, is a + * malformed-Unicode condition; on the wire it surfaces at Stage 5 as + * {@code E_SCHEMA_MALFORMED_UNICODE}. Since this is detected during string + * tokenizing, the parser raises that Stage 5 code directly so the + * first-failing-stage report is correct (the structural JSON is otherwise + * well formed). A non-hex {@code \\u} body is structural JSON malformation + * and is reported as {@code E_PARSE_JSON}. + */ + private char[] parseUnicodeEscape() { + char first = readHex4(); + if (Character.isHighSurrogate(first)) { + if (pos + 1 < s.length() && s.charAt(pos) == '\\' && s.charAt(pos + 1) == 'u') { + pos += 2; + char second = readHex4(); + if (Character.isLowSurrogate(second)) { + return new char[] {first, second}; + } + } + throw new RejectException(DiagnosticCode.E_SCHEMA_MALFORMED_UNICODE); + } + if (Character.isLowSurrogate(first)) { + // An unpaired low surrogate escape. + throw new RejectException(DiagnosticCode.E_SCHEMA_MALFORMED_UNICODE); + } + return new char[] {first}; + } + + private char readHex4() { + if (pos + 4 > s.length()) { + throw parseError(); + } + int v = 0; + for (int i = 0; i < 4; i++) { + char h = s.charAt(pos++); + int d = Character.digit(h, 16); + if (d < 0) { + throw parseError(); + } + v = (v << 4) | d; + } + return (char) v; + } + + private JsonValue parseNumber() { + int start = pos; + if (peek() == '-') { + pos++; + } + // integer part + if (pos >= s.length()) { + throw parseError(); + } + char d0 = s.charAt(pos); + if (d0 == '0') { + pos++; + } else if (d0 >= '1' && d0 <= '9') { + pos++; + while (pos < s.length() && isDigit(s.charAt(pos))) { + pos++; + } + } else { + throw parseError(); + } + // fraction + if (pos < s.length() && s.charAt(pos) == '.') { + pos++; + if (pos >= s.length() || !isDigit(s.charAt(pos))) { + throw parseError(); + } + while (pos < s.length() && isDigit(s.charAt(pos))) { + pos++; + } + } + // exponent + if (pos < s.length() && (s.charAt(pos) == 'e' || s.charAt(pos) == 'E')) { + pos++; + if (pos < s.length() && (s.charAt(pos) == '+' || s.charAt(pos) == '-')) { + pos++; + } + if (pos >= s.length() || !isDigit(s.charAt(pos))) { + throw parseError(); + } + while (pos < s.length() && isDigit(s.charAt(pos))) { + pos++; + } + } + String raw = s.substring(start, pos); + return classifyNumber(raw); + } + + /** Classify a well-formed JSON number token against the Entangled integer grammar (section 04). */ + static JsonValue.Num classifyNumber(String raw) { + boolean conforming = matchesIntegerGrammar(raw); + BigInteger value = null; + if (conforming) { + value = new BigInteger(raw); + if (value.compareTo(MAX_INT63) > 0) { + // Lexically integer-shaped but out of the [0, 2^63 - 1] range: + // not a conforming Entangled integer (section 04 range bound). + conforming = false; + value = null; + } + } + return new JsonValue.Num(raw, conforming, value); + } + + /** ABNF from section 04: integer = "0" / non-zero-digit *digit ; no sign, no leading zero. */ + private static boolean matchesIntegerGrammar(String raw) { + if (raw.isEmpty()) { + return false; + } + if (raw.equals("0")) { + return true; + } + char first = raw.charAt(0); + if (first < '1' || first > '9') { + return false; + } + for (int i = 1; i < raw.length(); i++) { + if (!isDigit(raw.charAt(i))) { + return false; + } + } + return true; + } + + private JsonValue parseBool() { + if (s.startsWith("true", pos)) { + pos += 4; + return new JsonValue.Bool(true); + } + if (s.startsWith("false", pos)) { + pos += 5; + return new JsonValue.Bool(false); + } + throw parseError(); + } + + private JsonValue parseNull() { + if (s.startsWith("null", pos)) { + pos += 4; + return new JsonValue.Null(); + } + throw parseError(); + } + + private void enterDepth(int depth) { + // depth is the count of containers already open; the new container makes depth+1. + if (depth + 1 > MAX_DEPTH) { + throw new RejectException(DiagnosticCode.E_PARSE_NESTING_DEPTH); + } + } + + private void skipWs() { + while (pos < s.length()) { + char c = s.charAt(pos); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + pos++; + } else { + break; + } + } + } + + private char peek() { + if (pos >= s.length()) { + throw parseError(); + } + return s.charAt(pos); + } + + private char next() { + if (pos >= s.length()) { + throw parseError(); + } + return s.charAt(pos++); + } + + private void expect(char c) { + if (pos >= s.length() || s.charAt(pos) != c) { + throw parseError(); + } + pos++; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + private static RejectException parseError() { + return new RejectException(DiagnosticCode.E_PARSE_JSON); + } +} diff --git a/src/main/java/org/entangled/json/JsonValue.java b/src/main/java/org/entangled/json/JsonValue.java new file mode 100644 index 0000000..98ab731 --- /dev/null +++ b/src/main/java/org/entangled/json/JsonValue.java @@ -0,0 +1,61 @@ +package org.entangled.json; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +/** + * A parsed JSON value, restricted to the forms Entangled documents may contain. + * + *

Numbers retain their raw lexical token text so that the Entangled integer + * grammar (section 04) can be enforced at schema validation (Stage 5) rather + * than during parsing: a well-formed JSON number that is a float, has an + * exponent, a leading zero, a sign, or is out of range is valid JSON but is + * rejected as {@code E_SCHEMA_NON_INTEGER} at Stage 5. The parser therefore + * accepts it lexically and defers the integer-grammar judgment. + * + *

The JSON literal {@code null} is likewise parsed (so the document is + * structurally understood) and rejected at Stage 5 as {@code E_SCHEMA_NULL_VALUE} + * (section 04, section 02). + */ +public sealed interface JsonValue + permits JsonValue.Obj, JsonValue.Arr, JsonValue.Str, JsonValue.Num, + JsonValue.Bool, JsonValue.Null { + + /** A JSON object. Member order is preserved as parsed; keys are unique (duplicates rejected at Stage 3). */ + record Obj(Map members) implements JsonValue { + public boolean has(String key) { + return members.containsKey(key); + } + + public JsonValue get(String key) { + return members.get(key); + } + } + + /** A JSON array. */ + record Arr(List elements) implements JsonValue { + } + + /** A JSON string, already decoded from its wire escapes to a Java String. */ + record Str(String value) implements JsonValue { + } + + /** + * A JSON number. {@code raw} is the exact lexical token as it appeared on the + * wire. {@code conformingInteger} is true only when {@code raw} matches the + * Entangled integer grammar (section 04): {@code 0 | [1-9][0-9]*}, no sign, + * no leading zero, no decimal point, no exponent, value in {@code [0, 2^63-1]}. + * When true, {@code value} holds the decoded magnitude. + */ + record Num(String raw, boolean conformingInteger, BigInteger value) implements JsonValue { + } + + /** A JSON boolean. */ + record Bool(boolean value) implements JsonValue { + } + + /** The JSON literal {@code null}; never valid in a conforming Entangled document. */ + record Null() implements JsonValue { + } +} diff --git a/src/main/java/org/entangled/pipeline/Context.java b/src/main/java/org/entangled/pipeline/Context.java new file mode 100644 index 0000000..f2cd992 --- /dev/null +++ b/src/main/java/org/entangled/pipeline/Context.java @@ -0,0 +1,66 @@ +package org.entangled.pipeline; + +import java.util.ArrayList; +import java.util.List; + +/** + * Per-evaluation context supplied to the validation pipeline. + * + *

This carries the out-of-band facts a real client would hold and that the + * corpus supplies through each vector's {@code context} block (corpus/README.md): + * the mocked clock, the carrier origin and path a document was fetched from, the + * submit body and submit path for transaction binding, the seeded publisher + * history (prior verified manifests) for anti-downgrade / canary-conflict / + * runtime-reuse, and the successor manifest material for migration scenarios. + * + *

The pipeline reads only what each document kind needs; absent fields are + * left null and the corresponding stage adapts (for example, a content document + * with no fetched path cannot run Stage 9 path binding). + */ +public final class Context { + + /** Mocked current time, epoch seconds (corpus clock_now). */ + public final long nowEpoch; + + /** + * The document kind the client expects from the fetch context (manifest from + * /manifest.json, content from a content path, transaction from a submit + * response). This selects the Stage 2 byte cap, which the spec enforces + * before parsing (section 10 Stage 2): a manifest is capped at 64 KiB, + * content/transaction at 1 MiB. A real client knows this from which endpoint + * it fetched; the corpus supplies it as the vector's {@code kind}. + */ + public Stage4Kind.Kind expectedKind; + + /** Carrier origin address a manifest was fetched from (Stage 9 origin binding). */ + public String fetchedOriginAddress; + + /** Path a content document was fetched from (Stage 9 path binding). */ + public String fetchedPath; + + /** + * The runtime public key the current manifest authorizes, supplied for + * content/transaction vectors (corpus context.expected_runtime_pubkey). + * Null means no verified manifest is available -> Stage 6 E_SIG_INVALID_KEY. + */ + public String expectedRuntimePubkey; + + /** Path a submit was sent to (Stage 9 transaction in_response_to binding). */ + public String submitPath; + + /** Exact bytes of the submit body the client sent (Stage 9 request_hash / request_id binding). */ + public byte[] submitBody; + + /** Prior verified manifests for the same publisher, oldest first (publisher history seed). */ + public final List publisherHistory = new ArrayList<>(); + + /** Successor manifest bytes for a migration scenario (Stage 9 successor verification). */ + public byte[] successorManifest; + + /** Announced successor origin address for a migration scenario. */ + public String successorOriginAddress; + + public Context(long nowEpoch) { + this.nowEpoch = nowEpoch; + } +} diff --git a/src/main/java/org/entangled/pipeline/Pipeline.java b/src/main/java/org/entangled/pipeline/Pipeline.java new file mode 100644 index 0000000..fdee07e --- /dev/null +++ b/src/main/java/org/entangled/pipeline/Pipeline.java @@ -0,0 +1,178 @@ +package org.entangled.pipeline; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import org.entangled.Diagnostic; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.Verdict; +import org.entangled.crypto.Base64Url; +import org.entangled.crypto.Ed25519; +import org.entangled.crypto.Sha; +import org.entangled.crypto.TorV3Address; +import org.entangled.json.Jcs; +import org.entangled.json.JsonParser; +import org.entangled.json.JsonValue; +import org.entangled.schema.DocumentSchema; +import org.entangled.schema.Rfc3339; + +/** + * The 10-stage validation pipeline (section 10), evaluating a single document + * (with its {@link Context}) to a {@link Verdict}. + * + *

Stages run in order and the first failing stage determines the rejection + * (section 10 error precedence): a {@link RejectException} thrown by any stage is + * caught here and converted to the verdict. Stages that depend on an + * already-verified manifest are adapted per document kind. + * + *

Stage 1 (transport) is not exercised by the corpus (wire-body only, no + * response metadata), so it is not modeled here; the byte cap is selected by + * document kind at Stage 2. + */ +public final class Pipeline { + + private static final String CTX_MANIFEST = "ENTANGLED-v1 manifest"; + private static final String CTX_CONTENT = "ENTANGLED-v1 content"; + private static final String CTX_TRANSACTION = "ENTANGLED-v1 transaction"; + + private final Context ctx; + + public Pipeline(Context ctx) { + this.ctx = ctx; + } + + /** Run the pipeline on the raw document bytes. */ + public Verdict run(byte[] body) { + try { + // Stage 2: the byte cap is document-kind specific and is enforced + // before parsing (section 10 Stage 2). The expected kind comes from + // the fetch context (a real client knows it fetched /manifest.json, + // a content path, or a submit response); the corpus supplies it as + // the vector's kind. When unknown, fall back to the most permissive + // 1 MiB cap so UTF-8/BOM are still checked before parse. + int cap = capForExpectedKind(); + String text = Stage2Input.validateAndDecode(body, cap); + + // Stage 3: parse. + JsonValue root = JsonParser.parse(text); + + // Stage 4: kind discrimination. + Stage4Kind.Kind kind = Stage4Kind.discriminate(root); + JsonValue.Obj doc = (JsonValue.Obj) root; + + switch (kind) { + case MANIFEST -> runManifest(doc, body); + case CONTENT -> runContent(doc, body); + case TRANSACTION -> runTransaction(doc, body); + } + return Verdict.accept(); + } catch (RejectException e) { + return Verdict.reject(e.diagnostic()); + } + } + + private int capForExpectedKind() { + if (ctx.expectedKind == null) { + return Stage2Input.CONTENT_BYTE_CAP; + } + return switch (ctx.expectedKind) { + case MANIFEST -> Stage2Input.MANIFEST_BYTE_CAP; + case CONTENT -> Stage2Input.CONTENT_BYTE_CAP; + case TRANSACTION -> Stage2Input.TRANSACTION_BYTE_CAP; + }; + } + + // --- Manifest --- + + private void runManifest(JsonValue.Obj doc, byte[] body) { + // Stage 5: closed schema + cross-field semantic checks. + DocumentSchema.validateManifest(doc, ctx.nowEpoch); + + // Stage 6: signature under publisher_pubkey (first contact uses the + // manifest's own key; the corpus single-document manifests have no + // retained identity to mismatch against). + byte[] pub = decode32(strField(doc, "publisher_pubkey")); + verifyOrThrow(pub, doc, CTX_MANIFEST); + + // Stage 8: canary state, future-skew, anti-downgrade, conflict, runtime reuse. + Stage8Canary.evaluate(doc, ctx); + + // Stage 9: origin binding, not_after expiry, migration. + Stage9Binding.manifest(doc, ctx); + } + + // --- Content --- + + private void runContent(JsonValue.Obj doc, byte[] body) { + DocumentSchema.validateContent(doc); + + // Stage 6: verify under the authorized runtime key. The corpus supplies + // it via context.expected_runtime_pubkey (the key the current manifest + // would authorize). Absence means no verified manifest -> E_SIG_INVALID_KEY. + byte[] runtimePub = runtimeKeyOrInvalid(); + verifyOrThrow(runtimePub, doc, CTX_CONTENT); + + // Stage 9: path binding (byte-exact against the fetched path). + Stage9Binding.contentPath(doc, ctx); + } + + // --- Transaction --- + + private void runTransaction(JsonValue.Obj doc, byte[] body) { + DocumentSchema.validateTransaction(doc); + + byte[] runtimePub = runtimeKeyOrInvalid(); + verifyOrThrow(runtimePub, doc, CTX_TRANSACTION); + + // Stage 9: in_response_to / request_id / request_hash binding. + Stage9Binding.transaction(doc, ctx); + } + + private byte[] runtimeKeyOrInvalid() { + if (ctx.expectedRuntimePubkey == null) { + // No verified manifest from which to obtain the authorized runtime key. + throw new RejectException(DiagnosticCode.E_SIG_INVALID_KEY); + } + return decode32(ctx.expectedRuntimePubkey); + } + + // --- signature helpers --- + + /** Verify the document signature; throw E_SIG_VERIFICATION on failure. */ + static void verifyOrThrow(byte[] pub, JsonValue.Obj doc, String context) { + byte[] sig = decode64(strField(doc, "sig")); + byte[] input = signatureInput(context, canonicalPayloadMinusSig(doc)); + if (!Ed25519.verify(pub, sig, input)) { + throw new RejectException(DiagnosticCode.E_SIG_VERIFICATION); + } + } + + /** JCS of the document object with the top-level sig removed. */ + static byte[] canonicalPayloadMinusSig(JsonValue.Obj doc) { + Map members = new LinkedHashMap<>(doc.members()); + members.remove("sig"); + return Jcs.canonicalize(new JsonValue.Obj(members)); + } + + static byte[] signatureInput(String context, byte[] jcs) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.writeBytes(context.getBytes(StandardCharsets.US_ASCII)); + out.write(0x00); + out.writeBytes(jcs); + return out.toByteArray(); + } + + static String strField(JsonValue.Obj doc, String key) { + return ((JsonValue.Str) doc.get(key)).value(); + } + + static byte[] decode32(String b64u) { + return Base64Url.decode(b64u, 32); + } + + static byte[] decode64(String b64u) { + return Base64Url.decode(b64u, 64); + } +} diff --git a/src/main/java/org/entangled/pipeline/Stage2Input.java b/src/main/java/org/entangled/pipeline/Stage2Input.java new file mode 100644 index 0000000..145aa96 --- /dev/null +++ b/src/main/java/org/entangled/pipeline/Stage2Input.java @@ -0,0 +1,76 @@ +package org.entangled.pipeline; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; + +/** + * Stage 2 byte-level checks (section 10, section 04): + *

+ * + *

The cap is checked first: a response that exceeds its cap is rejected + * without further inspection. BOM is checked before UTF-8 decoding because a + * BOM is well-formed UTF-8 but is its own rejection condition (section 04 "No + * BOM"); ordering it first yields {@code E_INPUT_BOM} rather than letting it + * pass UTF-8 validation. + */ +public final class Stage2Input { + + /** Document-kind byte caps (section 02, section 06, section 09). */ + public static final int MANIFEST_BYTE_CAP = 64 * 1024; + public static final int CONTENT_BYTE_CAP = 1024 * 1024; + public static final int TRANSACTION_BYTE_CAP = 1024 * 1024; + public static final int SUBMIT_BYTE_CAP = 64 * 1024; + + private static final byte[] UTF8_BOM = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; + + private Stage2Input() { + } + + /** + * Validate raw body bytes and decode to a String for parsing. + * + * @param body the exact response body bytes + * @param byteCap the document-kind cap to enforce before any decoding + * @return the decoded UTF-8 String, guaranteed BOM-free and strict UTF-8 + */ + public static String validateAndDecode(byte[] body, int byteCap) { + if (body.length > byteCap) { + throw new RejectException(DiagnosticCode.E_INPUT_BYTE_CAP); + } + if (startsWithBom(body)) { + throw new RejectException(DiagnosticCode.E_INPUT_BOM); + } + return decodeStrictUtf8(body); + } + + private static boolean startsWithBom(byte[] body) { + if (body.length < UTF8_BOM.length) { + return false; + } + for (int i = 0; i < UTF8_BOM.length; i++) { + if (body[i] != UTF8_BOM[i]) { + return false; + } + } + return true; + } + + private static String decodeStrictUtf8(byte[] body) { + var decoder = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + try { + return decoder.decode(java.nio.ByteBuffer.wrap(body)).toString(); + } catch (CharacterCodingException e) { + throw new RejectException(DiagnosticCode.E_INPUT_UTF8); + } + } +} diff --git a/src/main/java/org/entangled/pipeline/Stage4Kind.java b/src/main/java/org/entangled/pipeline/Stage4Kind.java new file mode 100644 index 0000000..32c48bb --- /dev/null +++ b/src/main/java/org/entangled/pipeline/Stage4Kind.java @@ -0,0 +1,58 @@ +package org.entangled.pipeline; + +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.json.JsonValue; + +/** + * Stage 4 document-kind discrimination (section 10, section 02): + *

+ * + *

This obtains the minimum needed to select a schema for Stage 5; full + * closed-schema validation happens there. + */ +public final class Stage4Kind { + + /** The three document kinds. */ + public enum Kind { MANIFEST, CONTENT, TRANSACTION } + + private Stage4Kind() { + } + + public static Kind discriminate(JsonValue root) { + if (!(root instanceof JsonValue.Obj obj)) { + // The top-level document must be a JSON object. + throw new RejectException(DiagnosticCode.E_KIND_MISSING_FIELDS); + } + String specVersion = requireString(obj, "spec_version"); + requireString(obj, "kind"); + requireString(obj, "sig"); + + if (!specVersion.equals("1.0")) { + throw new RejectException(DiagnosticCode.E_KIND_SPEC_VERSION); + } + String kind = ((JsonValue.Str) obj.get("kind")).value(); + return switch (kind) { + case "manifest" -> Kind.MANIFEST; + case "content" -> Kind.CONTENT; + case "transaction" -> Kind.TRANSACTION; + default -> throw new RejectException(DiagnosticCode.E_KIND_UNKNOWN); + }; + } + + private static String requireString(JsonValue.Obj obj, String key) { + JsonValue v = obj.get(key); + if (!(v instanceof JsonValue.Str s)) { + // Absent or wrong primitive type. + throw new RejectException(DiagnosticCode.E_KIND_MISSING_FIELDS); + } + return s.value(); + } +} diff --git a/src/main/java/org/entangled/pipeline/Stage8Canary.java b/src/main/java/org/entangled/pipeline/Stage8Canary.java new file mode 100644 index 0000000..6da14ca --- /dev/null +++ b/src/main/java/org/entangled/pipeline/Stage8Canary.java @@ -0,0 +1,143 @@ +package org.entangled.pipeline; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.json.JsonParser; +import org.entangled.json.JsonValue; +import org.entangled.schema.Rfc3339; + +/** + * Stage 8: canary and anti-downgrade resolution (section 08, section 10). + * + *

Structural canary validity (field shapes, interval 7..30 days) is enforced + * at Stage 5; this stage adds the time- and history-dependent checks: + *

+ */ +public final class Stage8Canary { + + private Stage8Canary() { + } + + /** A minimal projection of a verified manifest needed for Stage 8 history checks. */ + private record HistoryEntry(String publisherPubkey, String runtimePubkey, + long issuedAt, String jcsPayload) { + } + + static void evaluate(JsonValue.Obj doc, Context ctx) { + JsonValue.Obj canary = (JsonValue.Obj) doc.get("canary"); + String issuedAtStr = ((JsonValue.Str) canary.get("issued_at")).value(); + long issuedAt = Rfc3339.epochSeconds(issuedAtStr); + + // issued_at future-skew beyond the 300s tolerance is a canary Invalid + // condition (section 08), distinct from the section 05 signature checks. + if (issuedAt > ctx.nowEpoch + DocumentSchema_SKEW) { + throw new RejectException(DiagnosticCode.E_CANARY_INVALID); + } + + String publisherPubkey = ((JsonValue.Str) doc.get("publisher_pubkey")).value(); + String runtimePubkey = ((JsonValue.Str) canary.get("runtime_pubkey")).value(); + String currentPayload = new String(Pipeline.canonicalPayloadMinusSig(doc), StandardCharsets.UTF_8); + + List history = parseHistory(ctx); + if (history.isEmpty()) { + return; + } + + // Anti-downgrade and equal-issued_at conflict are evaluated against the + // newest verified issued_at for the same publisher. + HistoryEntry newest = null; + for (HistoryEntry h : history) { + if (!h.publisherPubkey.equals(publisherPubkey)) { + continue; + } + if (newest == null || h.issuedAt > newest.issuedAt) { + newest = h; + } + } + if (newest != null) { + if (issuedAt < newest.issuedAt) { + throw new RejectException(DiagnosticCode.E_CANARY_DOWNGRADE); + } + if (issuedAt == newest.issuedAt && !currentPayload.equals(newest.jcsPayload)) { + Map details = new LinkedHashMap<>(); + details.put("issued_at", issuedAtStr); + details.put("retained_runtime_pubkey", newest.runtimePubkey); + details.put("presented_runtime_pubkey", runtimePubkey); + throw new RejectException(DiagnosticCode.E_CANARY_CONFLICT, details); + } + } + + // Runtime-key reuse. The immediately preceding entry (last in history, + // for the same publisher) is the MUST check (window_position 1); any + // earlier match is the SHOULD check (window_position >= 2). History is + // ordered oldest-first, so index from the end. + checkRuntimeReuse(history, publisherPubkey, runtimePubkey, issuedAtStr); + } + + private static void checkRuntimeReuse(List history, String publisherPubkey, + String runtimePubkey, String currentIssuedAt) { + // Build the per-publisher list in chronological order. + List own = new ArrayList<>(); + for (HistoryEntry h : history) { + if (h.publisherPubkey.equals(publisherPubkey)) { + own.add(h); + } + } + // window_position: 1 = immediately preceding (last), 2 = the one before, ... + for (int i = own.size() - 1; i >= 0; i--) { + HistoryEntry h = own.get(i); + if (h.runtimePubkey.equals(runtimePubkey)) { + int windowPosition = own.size() - i; + Map details = new LinkedHashMap<>(); + details.put("runtime_pubkey", runtimePubkey); + details.put("previous_issued_at", isoOf(h.issuedAt)); + details.put("current_issued_at", currentIssuedAt); + details.put("window_position", (long) windowPosition); + throw new RejectException(DiagnosticCode.E_CANARY_RUNTIME_REUSE, details); + } + } + } + + private static List parseHistory(Context ctx) { + List entries = new ArrayList<>(); + for (byte[] manifestBytes : ctx.publisherHistory) { + String text = new String(manifestBytes, StandardCharsets.UTF_8); + JsonValue.Obj m = (JsonValue.Obj) JsonParser.parse(text); + JsonValue.Obj canary = (JsonValue.Obj) m.get("canary"); + String pub = ((JsonValue.Str) m.get("publisher_pubkey")).value(); + String rt = ((JsonValue.Str) canary.get("runtime_pubkey")).value(); + long issuedAt = Rfc3339.epochSeconds(((JsonValue.Str) canary.get("issued_at")).value()); + String payload = new String(Pipeline.canonicalPayloadMinusSig(m), StandardCharsets.UTF_8); + entries.add(new HistoryEntry(pub, rt, issuedAt, payload)); + } + return entries; + } + + private static String isoOf(long epochSeconds) { + return java.time.Instant.ofEpochSecond(epochSeconds) + .atOffset(java.time.ZoneOffset.UTC) + .format(java.time.format.DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'")); + } + + // Section 10 clock-skew tolerance, mirrored here to avoid a cross-package + // constant dependency cycle; both reference the same normative 300 seconds. + private static final long DocumentSchema_SKEW = 300; +} diff --git a/src/main/java/org/entangled/pipeline/Stage9Binding.java b/src/main/java/org/entangled/pipeline/Stage9Binding.java new file mode 100644 index 0000000..01c45a2 --- /dev/null +++ b/src/main/java/org/entangled/pipeline/Stage9Binding.java @@ -0,0 +1,244 @@ +package org.entangled.pipeline; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.Verdict; +import org.entangled.crypto.Base64Url; +import org.entangled.crypto.Sha; +import org.entangled.crypto.TorV3Address; +import org.entangled.json.Jcs; +import org.entangled.json.JsonParser; +import org.entangled.json.JsonValue; +import org.entangled.schema.Rfc3339; + +/** + * Stage 9: path and origin binding (section 10), plus the manifest origin + * lifecycle and migration checks (section 06, section 10). + * + *

Manifest: Tor v3 origin binding ({@code E_BIND_ORIGIN}); {@code origin.not_after} + * expiry with the symmetric 300s past-bound tolerance ({@code E_ORIGIN_EXPIRED}); + * and, when a {@code migration_pointer} is present, the announcement-internal + * successor key binding ({@code E_MIGRATION_INVALID} reason + * {@code successor_key_mismatch}), the self-pointer / cycle checks + * ({@code E_MIGRATION_INVALID}), and the fetched-successor verification + * ({@code E_MIGRATION_MISMATCH}). + * + *

Content: byte-exact {@code path} binding ({@code E_BIND_PATH}). Transaction: + * {@code in_response_to} ({@code E_BIND_RESPONSE_PATH}), {@code request_id} + * ({@code E_BIND_REQUEST_ID}), and {@code request_hash} ({@code E_BIND_REQUEST_HASH}). + */ +public final class Stage9Binding { + + /** Section 10 past-bound tolerance for origin.not_after. */ + private static final long SKEW = 300; + + private Stage9Binding() { + } + + // --- manifest --- + + static void manifest(JsonValue.Obj doc, Context ctx) { + JsonValue.Obj origin = (JsonValue.Obj) doc.get("origin"); + bindOrigin(origin, ctx.fetchedOriginAddress); + notAfterExpiry(origin, ctx.nowEpoch); + + if (doc.has("migration_pointer")) { + migration(doc, ctx); + } + } + + /** Tor v3 origin binding: fetched address decodes to a key equal to origin.origin_pubkey. */ + private static void bindOrigin(JsonValue.Obj origin, String fetchedAddress) { + if (fetchedAddress == null) { + return; // no fetched origin supplied; binding not evaluated + } + String declaredAddress = ((JsonValue.Str) origin.get("address")).value(); + String declaredPubkeyB64u = ((JsonValue.Str) origin.get("origin_pubkey")).value(); + // Fetched address must equal the declared address (canonical form). + if (!declaredAddress.equals(fetchedAddress)) { + throw new RejectException(DiagnosticCode.E_BIND_ORIGIN); + } + byte[] declaredPubkey = Base64Url.decode(declaredPubkeyB64u, 32); + byte[] derived; + try { + derived = TorV3Address.decodePublicKey(fetchedAddress); + } catch (TorV3Address.InvalidOnionAddress e) { + throw new RejectException(DiagnosticCode.E_BIND_ORIGIN); + } + if (!Arrays.equals(declaredPubkey, derived)) { + throw new RejectException(DiagnosticCode.E_BIND_ORIGIN); + } + } + + private static void notAfterExpiry(JsonValue.Obj origin, long nowEpoch) { + if (!origin.members().containsKey("not_after")) { + return; + } + long notAfter = Rfc3339.epochSeconds(((JsonValue.Str) origin.get("not_after")).value()); + // Section 10 past-bound rejection: current_time > not_after + 300 (strict). + if (nowEpoch > notAfter + SKEW) { + Map details = new LinkedHashMap<>(); + details.put("not_after", ((JsonValue.Str) origin.get("not_after")).value()); + details.put("now", isoMinute(nowEpoch)); + throw new RejectException(DiagnosticCode.E_ORIGIN_EXPIRED, details); + } + } + + // --- migration --- + + private static void migration(JsonValue.Obj doc, Context ctx) { + JsonValue.Obj origin = (JsonValue.Obj) doc.get("origin"); + String announcingAddress = ((JsonValue.Str) origin.get("address")).value(); + JsonValue.Obj mp = (JsonValue.Obj) doc.get("migration_pointer"); + JsonValue.Obj successorOrigin = (JsonValue.Obj) mp.get("successor_origin"); + String successorAddress = ((JsonValue.Str) successorOrigin.get("address")).value(); + String successorPubkeyB64u = ((JsonValue.Str) successorOrigin.get("origin_pubkey")).value(); + String announcedAt = ((JsonValue.Str) mp.get("announced_at")).value(); + String updated = ((JsonValue.Str) doc.get("updated")).value(); + + // Self-pointer: successor address equals announcing address. + if (successorAddress.equals(announcingAddress)) { + throw migrationInvalid("self_pointer", announcingAddress, successorAddress); + } + // announced_at must not be later than updated. + if (Rfc3339.epochSeconds(announcedAt) > Rfc3339.epochSeconds(updated)) { + throw migrationInvalid("announced_at_after_updated", announcingAddress, successorAddress); + } + // carrier match (both must be tor-v3; schema already enforced tor-v3). + String announcingCarrier = ((JsonValue.Str) origin.get("carrier")).value(); + String successorCarrier = ((JsonValue.Str) successorOrigin.get("carrier")).value(); + if (!announcingCarrier.equals(successorCarrier)) { + throw migrationInvalid("carrier_mismatch", announcingAddress, successorAddress); + } + // Announcement-internal: successor address must decode to successor origin_pubkey. + byte[] declaredSuccessorPubkey = Base64Url.decode(successorPubkeyB64u, 32); + byte[] derived; + try { + derived = TorV3Address.decodePublicKey(successorAddress); + } catch (TorV3Address.InvalidOnionAddress e) { + throw migrationInvalid("successor_key_mismatch", announcingAddress, successorAddress); + } + if (!Arrays.equals(declaredSuccessorPubkey, derived)) { + throw migrationInvalid("successor_key_mismatch", announcingAddress, successorAddress); + } + + // Fetch-time successor verification (when the successor manifest is supplied). + if (ctx.successorManifest != null) { + verifySuccessor(doc, successorOrigin, successorAddress, announcingAddress, ctx); + } + } + + private static void verifySuccessor(JsonValue.Obj announcing, JsonValue.Obj successorOrigin, + String successorAddress, String announcingAddress, Context ctx) { + // Per-flow visited-origins begins with the announcing origin; a successor + // address already visited is a chain cycle. + if (successorAddress.equals(announcingAddress)) { + throw migrationInvalid("chain_cycle", announcingAddress, successorAddress); + } + + // Run the successor manifest through the full pipeline in isolation. + Context successorCtx = new Context(ctx.nowEpoch); + successorCtx.fetchedOriginAddress = successorAddress; + // The successor may itself announce a migration; detect a cycle back to + // the announcing origin before recursing into its own migration step. + JsonValue.Obj successorDoc = + (JsonValue.Obj) JsonParser.parse(new String(ctx.successorManifest, StandardCharsets.UTF_8)); + if (successorDoc.has("migration_pointer")) { + JsonValue.Obj sMp = (JsonValue.Obj) successorDoc.get("migration_pointer"); + JsonValue.Obj sSucc = (JsonValue.Obj) sMp.get("successor_origin"); + String sSuccAddr = ((JsonValue.Str) sSucc.get("address")).value(); + if (sSuccAddr.equals(announcingAddress)) { + // A -> B -> A: the successor announces a return to the announcing origin. + throw migrationInvalid("chain_cycle", successorAddress, announcingAddress); + } + } + + Verdict successorVerdict = new Pipeline(successorCtx).run(ctx.successorManifest); + if (!successorVerdict.isAccepted()) { + // The successor fails its own pipeline; surface as E_MIGRATION_MISMATCH + // with the underlying code. + Map details = new LinkedHashMap<>(); + details.put("mismatch_field", "successor_stage9_failure"); + details.put("underlying_diagnostic_code", successorVerdict.diagnostic().code().name()); + throw new RejectException(DiagnosticCode.E_MIGRATION_MISMATCH, details); + } + + // Publisher continuity and binding-field equality (checks 3 and 4). + String announcingPub = ((JsonValue.Str) announcing.get("publisher_pubkey")).value(); + String successorPub = ((JsonValue.Str) successorDoc.get("publisher_pubkey")).value(); + if (!announcingPub.equals(successorPub)) { + throw mismatch("publisher_pubkey"); + } + JsonValue.Obj successorDocOrigin = (JsonValue.Obj) successorDoc.get("origin"); + String successorDocAddress = ((JsonValue.Str) successorDocOrigin.get("address")).value(); + if (!successorDocAddress.equals(((JsonValue.Str) successorOrigin.get("address")).value())) { + throw mismatch("address"); + } + String successorDocPubkey = ((JsonValue.Str) successorDocOrigin.get("origin_pubkey")).value(); + if (!successorDocPubkey.equals(((JsonValue.Str) successorOrigin.get("origin_pubkey")).value())) { + throw mismatch("origin_pubkey"); + } + } + + // --- content / transaction --- + + static void contentPath(JsonValue.Obj doc, Context ctx) { + if (ctx.fetchedPath == null) { + return; + } + String declared = ((JsonValue.Str) doc.get("path")).value(); + if (!declared.equals(ctx.fetchedPath)) { + throw new RejectException(DiagnosticCode.E_BIND_PATH); + } + } + + static void transaction(JsonValue.Obj doc, Context ctx) { + if (ctx.submitPath != null) { + String inResponseTo = ((JsonValue.Str) doc.get("in_response_to")).value(); + if (!inResponseTo.equals(ctx.submitPath)) { + throw new RejectException(DiagnosticCode.E_BIND_RESPONSE_PATH); + } + } + if (ctx.submitBody != null) { + // request_hash = "sha-256:" || base64url(SHA-256(JCS(submit_body))). + JsonValue submitParsed = JsonParser.parse(new String(ctx.submitBody, StandardCharsets.UTF_8)); + byte[] canonical = Jcs.canonicalize(submitParsed); + String expectedHash = "sha-256:" + base64urlNoPad(Sha.sha256(canonical)); + String declaredHash = ((JsonValue.Str) doc.get("request_hash")).value(); + if (!declaredHash.equals(expectedHash)) { + throw new RejectException(DiagnosticCode.E_BIND_REQUEST_HASH); + } + } + } + + // --- helpers --- + + private static RejectException migrationInvalid(String reason, String announcing, String successor) { + Map details = new LinkedHashMap<>(); + details.put("reason", reason); + details.put("announcing_origin_address", announcing); + details.put("successor_origin_address", successor); + return new RejectException(DiagnosticCode.E_MIGRATION_INVALID, details); + } + + private static RejectException mismatch(String field) { + Map details = new LinkedHashMap<>(); + details.put("mismatch_field", field); + return new RejectException(DiagnosticCode.E_MIGRATION_MISMATCH, details); + } + + private static String isoMinute(long epochSeconds) { + long minute = (epochSeconds / 60) * 60; + return java.time.Instant.ofEpochSecond(minute) + .atOffset(java.time.ZoneOffset.UTC) + .format(java.time.format.DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:'00Z'")); + } + + private static String base64urlNoPad(byte[] data) { + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(data); + } +} diff --git a/src/main/java/org/entangled/schema/Blocks.java b/src/main/java/org/entangled/schema/Blocks.java new file mode 100644 index 0000000..bb38541 --- /dev/null +++ b/src/main/java/org/entangled/schema/Blocks.java @@ -0,0 +1,264 @@ +package org.entangled.schema; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.json.JsonValue; + +/** + * Block-grammar validation for the eleven block kinds, section 03. + * + *

Each block is validated against the closed schema for its declared + * {@code kind}; an unknown kind is {@code E_SCHEMA_ENUM_VIOLATION}. A + * {@code submit_form} block in a transaction document is + * {@code E_SCHEMA_BLOCK_NOT_PERMITTED}. Inline content is validated by + * {@link Inline}; per-block aggregate byte caps are enforced here. + */ +public final class Blocks { + + private static final Set KNOWN_KINDS = Set.of( + "paragraph", "heading", "code_block", "quote", "list", "divider", + "image", "link", "submit_form", "feedback", "note"); + + private static final Set FEEDBACK_VARIANTS = Set.of("success", "info", "warning", "error"); + private static final Set NOTE_VARIANTS = Set.of("info", "warning", "danger", "success"); + private static final Set MEDIA_TYPES = Set.of("image/png", "image/jpeg", "image/webp"); + private static final Set FIELD_KINDS = Set.of("text", "textarea", "select", "checkbox"); + + private Blocks() { + } + + /** Validate one block. {@code transaction} selects document-kind permission rules. */ + public static void validate(JsonValue blockValue, boolean transaction) { + JsonValue.Obj b = Fields.obj(blockValue); + String kind = Fields.str(Inline.require(b, "kind")); + if (!KNOWN_KINDS.contains(kind)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + if (transaction && kind.equals("submit_form")) { + throw new RejectException(DiagnosticCode.E_SCHEMA_BLOCK_NOT_PERMITTED); + } + switch (kind) { + case "paragraph" -> paragraph(b); + case "heading" -> heading(b); + case "code_block" -> codeBlock(b); + case "quote" -> quote(b); + case "list" -> list(b); + case "divider" -> divider(b); + case "image" -> image(b); + case "link" -> link(b); + case "submit_form" -> submitForm(b); + case "feedback" -> feedback(b); + case "note" -> note(b); + default -> throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + } + + private static void paragraph(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "content"), Set.of("kind", "content")); + int bytes = Inline.validate(b.get("content"), true, true); + cap(bytes, 8 * 1024); + } + + private static void heading(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "level", "content"), Set.of("kind", "level", "content")); + Fields.integerInRange(b.get("level"), 1, 6); + int bytes = Inline.validate(b.get("content"), true, true); + cap(bytes, 200); + } + + private static void codeBlock(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "language", "content"), Set.of("kind", "language", "content")); + slugLanguage(Fields.str(b.get("language"))); + String content = Fields.str(b.get("content")); + if (Fields.utf8Len(content) > 32 * 1024) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + // Control chars other than line feed are forbidden; NFC required (section 04). + Fields.noControlChars(content, true); + Fields.requireNfc(content); + } + + private static void quote(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "content", "attribution"), Set.of("kind", "content")); + int bytes = Inline.validate(b.get("content"), true, true); + cap(bytes, 4 * 1024); + if (b.has("attribution")) { + int abytes = Inline.validate(b.get("attribution"), true, true); + cap(abytes, 200); + } + } + + private static void list(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "ordered", "items"), Set.of("kind", "ordered", "items")); + Fields.bool(b.get("ordered")); + List items = Fields.arr(b.get("items")).elements(); + if (items.isEmpty() || items.size() > 64) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + int total = 0; + for (JsonValue item : items) { + total += Inline.validate(item, true, true); + } + cap(total, 8 * 1024); + } + + private static void divider(JsonValue.Obj b) { + Closed.check(b, Set.of("kind"), Set.of("kind")); + } + + private static void image(JsonValue.Obj b) { + Closed.check(b, + Set.of("kind", "src", "sha256", "media_type", "width", "height", "alt", "caption"), + Set.of("kind", "src", "sha256", "media_type", "width", "height", "alt")); + Fields.path(Fields.str(b.get("src"))); + sha256Field(Fields.str(b.get("sha256"))); + Fields.inEnum(Fields.str(b.get("media_type")), MEDIA_TYPES); + Fields.integerInRange(b.get("width"), 1, 4096); + Fields.integerInRange(b.get("height"), 1, 4096); + String alt = Fields.str(b.get("alt")); + Fields.maxBytes(alt, 1024); + Fields.noControlChars(alt, false); + Fields.requireNfc(alt); + if (b.has("caption")) { + String caption = Fields.str(b.get("caption")); + if (caption.isEmpty()) { + // An empty caption must be omitted, not present as "". + throw Fields.syntax(); + } + Fields.maxBytes(caption, 500); + Fields.noControlChars(caption, false); + Fields.requireNfc(caption); + } + } + + private static void link(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "label", "target"), Set.of("kind", "label", "target")); + // link.label is inline content that MUST NOT contain link elements. + int bytes = Inline.validate(b.get("label"), false, true); + cap(bytes, 200); + Inline.validateTarget(b.get("target")); + } + + private static void submitForm(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "label", "submit_to", "fields", "submit_label"), + Set.of("kind", "label", "submit_to", "fields", "submit_label")); + Inline.validate(b.get("label"), false, true); + Fields.path(Fields.str(b.get("submit_to"))); + List fields = Fields.arr(b.get("fields")).elements(); + if (fields.isEmpty() || fields.size() > 16) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + Set names = new HashSet<>(); + for (JsonValue f : fields) { + String name = formField(Fields.obj(f)); + if (!names.add(name)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_DUPLICATE_ENTRY); + } + } + String submitLabel = Fields.str(b.get("submit_label")); + Fields.maxBytes(submitLabel, 100); + Fields.noControlChars(submitLabel, false); + } + + private static String formField(JsonValue.Obj f) { + String kind = Fields.str(Inline.require(f, "kind")); + Fields.inEnum(kind, FIELD_KINDS); + String name; + switch (kind) { + case "text", "textarea" -> { + Closed.check(f, Set.of("kind", "name", "label", "required", "max_length"), + Set.of("kind", "name", "label", "required", "max_length")); + name = sharedFormFields(f); + Fields.integerInRange(f.get("max_length"), 1, 8192); + } + case "select" -> { + Closed.check(f, Set.of("kind", "name", "label", "required", "options"), + Set.of("kind", "name", "label", "required", "options")); + name = sharedFormFields(f); + selectOptions(f.get("options")); + } + case "checkbox" -> { + Closed.check(f, Set.of("kind", "name", "label", "required"), + Set.of("kind", "name", "label", "required")); + name = sharedFormFields(f); + } + default -> throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + return name; + } + + private static String sharedFormFields(JsonValue.Obj f) { + String name = Fields.str(f.get("name")); + Fields.slug(name, 64); + String label = Fields.str(f.get("label")); + Fields.maxBytes(label, 200); + Fields.noControlChars(label, false); + Fields.bool(f.get("required")); + return name; + } + + private static void selectOptions(JsonValue optionsValue) { + List options = Fields.arr(optionsValue).elements(); + if (options.isEmpty() || options.size() > 32) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + Set values = new HashSet<>(); + for (JsonValue o : options) { + JsonValue.Obj opt = Fields.obj(o); + Closed.check(opt, Set.of("value", "label"), Set.of("value", "label")); + String value = Fields.str(opt.get("value")); + Fields.slug(value, 64); + if (!values.add(value)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_DUPLICATE_ENTRY); + } + String label = Fields.str(opt.get("label")); + Fields.maxBytes(label, 200); + Fields.noControlChars(label, false); + } + } + + private static void feedback(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "variant", "content"), Set.of("kind", "variant", "content")); + Fields.inEnum(Fields.str(b.get("variant")), FEEDBACK_VARIANTS); + int bytes = Inline.validate(b.get("content"), true, true); + cap(bytes, 2 * 1024); + } + + private static void note(JsonValue.Obj b) { + Closed.check(b, Set.of("kind", "variant", "title", "content"), Set.of("kind", "variant", "content")); + Fields.inEnum(Fields.str(b.get("variant")), NOTE_VARIANTS); + if (b.has("title")) { + String title = Fields.str(b.get("title")); + if (title.isEmpty()) { + throw Fields.syntax(); + } + Fields.maxBytes(title, 200); + Fields.noControlChars(title, false); + Fields.requireNfc(title); + } + int bytes = Inline.validate(b.get("content"), true, true); + cap(bytes, 4 * 1024); + } + + private static void slugLanguage(String language) { + // code_block.language: [a-z0-9_-], begins [a-z0-9], non-empty, <= 64. + Fields.slug(language, 64); + } + + private static void sha256Field(String s) { + // "sha-256:" + 43 base64url chars = 51 chars total. + if (s.length() != 51 || !s.startsWith("sha-256:")) { + throw Fields.syntax(); + } + Fields.base64url(s.substring("sha-256:".length()), 32); + } + + private static void cap(int bytes, int max) { + if (bytes > max) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + } +} diff --git a/src/main/java/org/entangled/schema/Closed.java b/src/main/java/org/entangled/schema/Closed.java new file mode 100644 index 0000000..3033f1e --- /dev/null +++ b/src/main/java/org/entangled/schema/Closed.java @@ -0,0 +1,54 @@ +package org.entangled.schema; + +import java.util.Set; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.json.JsonValue; + +/** + * Closed-schema discipline for a single object level (section 02). + * + *

Given the permitted key set (required plus optional) and the required key + * set for an object, this verifies: + *

    + *
  • every present key is permitted, else {@code E_SCHEMA_UNKNOWN_FIELD};
  • + *
  • every required key is present, else {@code E_SCHEMA_REQUIRED_FIELD};
  • + *
  • no permitted present value is the JSON {@code null} literal, else + * {@code E_SCHEMA_NULL_VALUE} (section 04 forbids {@code null} anywhere).
  • + *
+ * + *

Order: unknown-field detection runs first (a stray key is a closed-schema + * breach regardless of its value), then required-presence, then the null check + * on the permitted values at this level. Nested objects run their own + * {@code Closed} check, so a {@code null} at any depth is caught at its level. + */ +public final class Closed { + + private Closed() { + } + + public static void check(JsonValue.Obj obj, Set permitted, Set required) { + for (String key : obj.members().keySet()) { + if (!permitted.contains(key)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_UNKNOWN_FIELD); + } + } + for (String key : required) { + if (!obj.members().containsKey(key)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_REQUIRED_FIELD); + } + } + for (JsonValue value : obj.members().values()) { + if (value instanceof JsonValue.Null) { + throw new RejectException(DiagnosticCode.E_SCHEMA_NULL_VALUE); + } + } + } + + /** Convenience: a value that must not be the null literal at this position. */ + public static void notNull(JsonValue value) { + if (value instanceof JsonValue.Null) { + throw new RejectException(DiagnosticCode.E_SCHEMA_NULL_VALUE); + } + } +} diff --git a/src/main/java/org/entangled/schema/DocumentSchema.java b/src/main/java/org/entangled/schema/DocumentSchema.java new file mode 100644 index 0000000..f0923f8 --- /dev/null +++ b/src/main/java/org/entangled/schema/DocumentSchema.java @@ -0,0 +1,393 @@ +package org.entangled.schema; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.json.JsonValue; + +/** + * Stage 5 closed-schema validation for the three document kinds (section 02, + * section 06, section 07, section 08), including the cross-field semantic checks + * the Stage 5 definition in section 10 assigns to this stage: + *

    + *
  • {@code origin.not_after} vs {@code canary.issued_at} bounds + * ({@code E_ORIGIN_INVALID}, section 06);
  • + *
  • the {@code state_policy} submit-budget satisfiability aggregate + * ({@code E_SUBMIT_BUDGET}, section 07/section 09);
  • + *
  • {@code manifest.updated} future-skew beyond the 300s tolerance + * ({@code E_SCHEMA_FIELD_SYNTAX} with {@code reason=future_beyond_skew_tolerance}, + * section 10).
  • + *
+ * + *

The signature ({@code sig}) is validated here only for base64url syntax and + * length (section 04 wire-side), so a malformed {@code sig} surfaces as + * {@code E_SCHEMA_FIELD_SYNTAX} at Stage 5 (vector 151), not as Stage 6 + * {@code E_SIG_MALFORMED}. + */ +public final class DocumentSchema { + + /** Section 10 clock-skew tolerance, seconds. */ + public static final long SKEW_TOLERANCE_SECONDS = 300; + + /** Section 09 submit-body budget partition: state_budget = 53248 bytes. */ + public static final int STATE_BUDGET_BYTES = 53248; + + /** Section 06 origin.not_after ceiling: 5 years in seconds. */ + public static final long NOT_AFTER_MAX_HORIZON_SECONDS = 157680000L; + + private static final Set STATE_MODES = Set.of("client_only", "request"); + + private DocumentSchema() { + } + + /** + * Document-wide numeric-grammar pre-scan (section 04). + * + *

Section 04 requires every numeric token to be validated against the + * integer grammar "at the lexical or parse level, before any conversion to a + * numeric type", independently of which fields a particular path reads. A + * non-conforming number anywhere in the document is therefore detected before + * closed-schema field-presence checks. Corpus vector 140 fixes this ordering: + * a manifest that both omits required fields and carries a float-shape token + * is rejected as E_SCHEMA_NON_INTEGER, not E_SCHEMA_REQUIRED_FIELD. We model + * the numeric grammar as a whole-document Stage 5 pre-pass to honor that. + * + *

(Ambiguity note: section 10's Stage 5 listing does not state the relative + * order of the numeric-grammar check and the required-field check when both + * fire. The chosen reading follows section 04's "before any conversion" + * mandate, treating the numeric grammar as a parse-adjacent whole-document + * check; vector 140 is the falsification condition. Filed as an ambiguity.) + */ + public static void scanNumericGrammar(JsonValue v) { + if (v instanceof JsonValue.Num n) { + if (!n.conformingInteger()) { + throw new RejectException(DiagnosticCode.E_SCHEMA_NON_INTEGER); + } + } else if (v instanceof JsonValue.Obj o) { + for (JsonValue child : o.members().values()) { + scanNumericGrammar(child); + } + } else if (v instanceof JsonValue.Arr a) { + for (JsonValue child : a.elements()) { + scanNumericGrammar(child); + } + } + } + + /** Validate a manifest payload (Stage 5). {@code nowEpoch} is the injected clock in epoch seconds. */ + public static void validateManifest(JsonValue.Obj doc, long nowEpoch) { + scanNumericGrammar(doc); + Closed.check(doc, + Set.of("spec_version", "kind", "publisher_pubkey", "origin", "canary", + "state_policy", "navigation", "min_refresh_interval", "updated", + "sig", "migration_pointer", "content_root"), + Set.of("spec_version", "kind", "publisher_pubkey", "origin", "canary", + "state_policy", "navigation", "min_refresh_interval", "updated", "sig")); + + Fields.base64url(Fields.str(doc.get("publisher_pubkey")), 32); + + long issuedAt = validateCanary(Fields.obj(doc.get("canary"))); + validateOrigin(Fields.obj(doc.get("origin")), issuedAt); + validateStatePolicy(Fields.arr(doc.get("state_policy"))); + validateNavigation(Fields.arr(doc.get("navigation"))); + Fields.integerInRange(doc.get("min_refresh_interval"), 300, 604800); + validateUpdated(Fields.str(doc.get("updated")), nowEpoch); + + if (doc.has("migration_pointer")) { + validateMigrationPointer(Fields.obj(doc.get("migration_pointer")), doc, issuedAt); + } + if (doc.has("content_root")) { + sha256Field(Fields.str(doc.get("content_root"))); + } + sig(Fields.str(doc.get("sig"))); + } + + /** Validate a content document payload (Stage 5). */ + public static void validateContent(JsonValue.Obj doc) { + scanNumericGrammar(doc); + Closed.check(doc, + Set.of("spec_version", "kind", "path", "meta", "blocks", "sig", "seq"), + Set.of("spec_version", "kind", "path", "meta", "blocks", "sig")); + Fields.path(Fields.str(doc.get("path"))); + validateMeta(Fields.obj(doc.get("meta"))); + validateBlocks(Fields.arr(doc.get("blocks")), false, 1024); + if (doc.has("seq")) { + // seq is a positive integer (>= 1). + Fields.integerInRange(doc.get("seq"), 1, Long.MAX_VALUE); + } + sig(Fields.str(doc.get("sig"))); + } + + /** Validate a transaction document payload (Stage 5). */ + public static void validateTransaction(JsonValue.Obj doc) { + scanNumericGrammar(doc); + Closed.check(doc, + Set.of("spec_version", "kind", "in_response_to", "request_id", + "request_hash", "state_updates", "blocks", "sig"), + Set.of("spec_version", "kind", "in_response_to", "request_id", + "request_hash", "state_updates", "blocks", "sig")); + Fields.path(Fields.str(doc.get("in_response_to"))); + // request_id: 22 base64url chars = 16 bytes. + Fields.base64url(Fields.str(doc.get("request_id")), 16); + sha256Field(Fields.str(doc.get("request_hash"))); + validateStateUpdates(Fields.arr(doc.get("state_updates"))); + validateBlocks(Fields.arr(doc.get("blocks")), true, 256); + sig(Fields.str(doc.get("sig"))); + } + + // --- manifest sub-objects --- + + /** Validate the canary object; returns canary.issued_at in epoch seconds. */ + private static long validateCanary(JsonValue.Obj canary) { + Closed.check(canary, + Set.of("runtime_pubkey", "issued_at", "next_expected", "statement", "freshness_proof"), + Set.of("runtime_pubkey", "issued_at", "next_expected", "statement")); + Fields.base64url(Fields.str(canary.get("runtime_pubkey")), 32); + + String issuedAtStr = Fields.str(canary.get("issued_at")); + String nextExpectedStr = Fields.str(canary.get("next_expected")); + // Timestamp syntax is a canary-structural concern; an invalid timestamp + // is reported as E_CANARY_INVALID at Stage 8, not E_SCHEMA_FIELD_SYNTAX. + if (!Rfc3339.isValid(issuedAtStr) || !Rfc3339.isValid(nextExpectedStr)) { + throw new RejectException(DiagnosticCode.E_CANARY_INVALID); + } + long issuedAt = Rfc3339.epochSeconds(issuedAtStr); + long nextExpected = Rfc3339.epochSeconds(nextExpectedStr); + // next_expected strictly later than issued_at; interval 7..30 days. + long interval = nextExpected - issuedAt; + if (interval < 604800 || interval > 2592000) { + throw new RejectException(DiagnosticCode.E_CANARY_INVALID); + } + + String statement = Fields.str(canary.get("statement")); + Fields.maxBytes(statement, 2048); + Fields.noControlChars(statement, true); // line feed allowed + Fields.requireNfc(statement); + + if (canary.has("freshness_proof")) { + String fp = Fields.str(canary.get("freshness_proof")); + Fields.maxBytes(fp, 200); + Fields.noControlChars(fp, false); + Fields.requireNfc(fp); + } + return issuedAt; + } + + private static void validateOrigin(JsonValue.Obj origin, long issuedAt) { + Closed.check(origin, + Set.of("carrier", "address", "origin_pubkey", "not_after"), + Set.of("carrier", "address", "origin_pubkey")); + // A non-tor-v3 carrier is a v1.0 rejection; treated as enum violation. + if (!Fields.str(origin.get("carrier")).equals("tor-v3")) { + throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + onionAddress(Fields.str(origin.get("address"))); + Fields.base64url(Fields.str(origin.get("origin_pubkey")), 32); + if (origin.has("not_after")) { + String notAfterStr = Fields.str(origin.get("not_after")); + Fields.rfc3339(notAfterStr); + long notAfter = Rfc3339.epochSeconds(notAfterStr); + // Cross-field semantic checks (E_ORIGIN_INVALID, section 06). + if (notAfter <= issuedAt) { + throw originInvalid("not_after_not_later_than_issued_at"); + } + if (notAfter - issuedAt > NOT_AFTER_MAX_HORIZON_SECONDS) { + throw originInvalid("not_after_beyond_5y"); + } + } + } + + private static void validateStatePolicy(JsonValue.Arr statePolicy) { + List entries = statePolicy.elements(); + if (entries.size() > 32) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + Set seen = new java.util.HashSet<>(); + long stateAggregate = 0; + int requestEntries = 0; + for (JsonValue e : entries) { + JsonValue.Obj entry = Fields.obj(e); + Closed.check(entry, + Set.of("namespace", "key", "mode", "max_size", "max_lifetime", "purpose"), + Set.of("namespace", "key", "mode", "max_size", "max_lifetime", "purpose")); + String namespace = Fields.str(entry.get("namespace")); + String key = Fields.str(entry.get("key")); + Fields.slug(namespace, 64); + Fields.slug(key, 64); + String composite = namespace + "" + key; + if (!seen.add(composite)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_DUPLICATE_ENTRY); + } + String mode = Fields.str(entry.get("mode")); + Fields.inEnum(mode, STATE_MODES); + long maxSize = Fields.integerInRange(entry.get("max_size"), 1, 4096); + Fields.integerInRange(entry.get("max_lifetime"), 300, 7776000); + String purpose = Fields.str(entry.get("purpose")); + Fields.maxBytes(purpose, 200); + Fields.noControlChars(purpose, false); + Fields.requireNfc(purpose); + + if (mode.equals("request")) { + // Section 09 per-entry wire contribution: a {"namespace":"","key":"", + // "value":""} skeleton is 36 bytes; add namespace, key, and the + // value counted at its raw max_size (section 07). + stateAggregate += 36L + namespace.length() + key.length() + maxSize; + requestEntries++; + } + } + if (requestEntries > 1) { + stateAggregate += (requestEntries - 1L); // inter-entry commas + } + if (stateAggregate > STATE_BUDGET_BYTES) { + Map details = new LinkedHashMap<>(); + details.put("component", "state"); + details.put("declared_bytes", stateAggregate); + details.put("budget_bytes", (long) STATE_BUDGET_BYTES); + throw new RejectException(DiagnosticCode.E_SUBMIT_BUDGET, details); + } + } + + private static void validateNavigation(JsonValue.Arr navigation) { + List entries = navigation.elements(); + if (entries.size() > 32) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + for (JsonValue e : entries) { + JsonValue.Obj entry = Fields.obj(e); + Closed.check(entry, Set.of("label", "path"), Set.of("label", "path")); + String label = Fields.str(entry.get("label")); + Fields.maxBytes(label, 100); + Fields.noControlChars(label, false); + Fields.requireNfc(label); + // navigation path uses content path syntax (reserved manifest path forbidden). + Fields.path(Fields.str(entry.get("path"))); + } + } + + private static void validateUpdated(String updated, long nowEpoch) { + Fields.rfc3339(updated); + long updatedEpoch = Rfc3339.epochSeconds(updated); + // Future-skew beyond 300s tolerance is a Stage 5 temporal-domain syntax + // failure (section 10): E_SCHEMA_FIELD_SYNTAX, reason future_beyond_skew_tolerance. + if (updatedEpoch > nowEpoch + SKEW_TOLERANCE_SECONDS) { + Map details = new LinkedHashMap<>(); + details.put("reason", "future_beyond_skew_tolerance"); + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_SYNTAX, details); + } + } + + private static void validateMigrationPointer(JsonValue.Obj mp, JsonValue.Obj manifest, long issuedAt) { + Closed.check(mp, Set.of("successor_origin", "announced_at"), + Set.of("successor_origin", "announced_at")); + JsonValue.Obj successor = Fields.obj(mp.get("successor_origin")); + Closed.check(successor, Set.of("carrier", "address", "origin_pubkey"), + Set.of("carrier", "address", "origin_pubkey")); + if (!Fields.str(successor.get("carrier")).equals("tor-v3")) { + throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + onionAddress(Fields.str(successor.get("address"))); + Fields.base64url(Fields.str(successor.get("origin_pubkey")), 32); + String announcedAt = Fields.str(mp.get("announced_at")); + Fields.rfc3339(announcedAt); + // Note: the semantic checks (self-pointer, announced_at vs updated, + // carrier match, successor address-to-key binding, chain cycle) are + // Stage 9 migration checks (E_MIGRATION_INVALID / E_MIGRATION_MISMATCH), + // handled by the pipeline, not here. + } + + // --- content sub-objects --- + + private static void validateMeta(JsonValue.Obj meta) { + Closed.check(meta, Set.of("title", "published_at"), Set.of("title", "published_at")); + String title = Fields.str(meta.get("title")); + Fields.maxBytes(title, 200); + Fields.noControlChars(title, false); + Fields.requireNfc(title); + Fields.rfc3339(Fields.str(meta.get("published_at"))); + } + + private static void validateBlocks(JsonValue.Arr blocks, boolean transaction, int maxBlocks) { + List list = blocks.elements(); + if (list.isEmpty()) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + if (list.size() > maxBlocks) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + for (JsonValue b : list) { + Blocks.validate(b, transaction); + } + } + + private static void validateStateUpdates(JsonValue.Arr updates) { + List list = updates.elements(); + if (list.size() > 32) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + for (JsonValue u : list) { + JsonValue.Obj op = Fields.obj(u); + String opName = Fields.str(Inline.require(op, "op")); + switch (opName) { + case "set" -> { + Closed.check(op, Set.of("op", "namespace", "key", "value", "ttl"), + Set.of("op", "namespace", "key", "value", "ttl")); + Fields.slug(Fields.str(op.get("namespace")), 64); + Fields.slug(Fields.str(op.get("key")), 64); + String value = Fields.str(op.get("value")); + if (Fields.utf8Len(value) > 4096) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + Fields.integerInRange(op.get("ttl"), 300, 7776000); + } + case "delete" -> { + Closed.check(op, Set.of("op", "namespace", "key"), + Set.of("op", "namespace", "key")); + Fields.slug(Fields.str(op.get("namespace")), 64); + Fields.slug(Fields.str(op.get("key")), 64); + } + default -> throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + } + } + + // --- shared --- + + private static void onionAddress(String address) { + if (!address.endsWith(".onion")) { + throw Fields.syntax(); + } + String body = address.substring(0, address.length() - ".onion".length()); + if (body.length() != 56) { + throw Fields.syntax(); + } + for (int i = 0; i < body.length(); i++) { + char c = body.charAt(i); + boolean ok = (c >= 'a' && c <= 'z') || (c >= '2' && c <= '7'); + if (!ok) { + throw Fields.syntax(); + } + } + } + + private static void sha256Field(String s) { + if (s.length() != 51 || !s.startsWith("sha-256:")) { + throw Fields.syntax(); + } + Fields.base64url(s.substring("sha-256:".length()), 32); + } + + private static void sig(String s) { + // sig is 86 base64url chars = 64 bytes; wire-side length/alphabet + // violations are E_SCHEMA_FIELD_SYNTAX at Stage 5 (vector 151, section 11). + Fields.base64url(s, 64); + } + + private static RejectException originInvalid(String reason) { + Map details = new LinkedHashMap<>(); + details.put("reason", reason); + return new RejectException(DiagnosticCode.E_ORIGIN_INVALID, details); + } +} diff --git a/src/main/java/org/entangled/schema/Fields.java b/src/main/java/org/entangled/schema/Fields.java new file mode 100644 index 0000000..f80dd29 --- /dev/null +++ b/src/main/java/org/entangled/schema/Fields.java @@ -0,0 +1,176 @@ +package org.entangled.schema; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; +import java.util.Set; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.crypto.Base64Url; +import org.entangled.json.JsonValue; + +/** + * Reusable Stage 5 field validators shared across document kinds (section 02, + * section 03, section 06, section 07, section 08). + * + *

Each method enforces one normative field constraint and throws a + * {@link RejectException} carrying the matching {@code E_SCHEMA_*} code. The + * closed-schema discipline (every present key must be permitted, every required + * key must be present, no {@code null}) is enforced by {@link Closed} together + * with these primitives. + * + *

Numeric handling: a value flagged non-conforming-integer by the parser + * (float, exponent, out of range) is {@code E_SCHEMA_NON_INTEGER}; a conforming + * integer outside a field's declared range is {@code E_SCHEMA_FIELD_RANGE} + * (section 11). + */ +public final class Fields { + + private Fields() { + } + + // --- type extraction --- + + public static JsonValue.Obj obj(JsonValue v) { + if (!(v instanceof JsonValue.Obj o)) { + throw type(); + } + return o; + } + + public static JsonValue.Arr arr(JsonValue v) { + if (!(v instanceof JsonValue.Arr a)) { + throw type(); + } + return a; + } + + public static String str(JsonValue v) { + if (!(v instanceof JsonValue.Str s)) { + throw type(); + } + return s.value(); + } + + public static boolean bool(JsonValue v) { + if (!(v instanceof JsonValue.Bool b)) { + throw type(); + } + return b.value(); + } + + /** Require a conforming Entangled integer; non-integers are E_SCHEMA_NON_INTEGER. */ + public static BigInteger integer(JsonValue v) { + if (!(v instanceof JsonValue.Num n)) { + throw type(); + } + if (!n.conformingInteger()) { + throw new RejectException(DiagnosticCode.E_SCHEMA_NON_INTEGER); + } + return n.value(); + } + + /** Require an integer in [min, max]; out-of-range is E_SCHEMA_FIELD_RANGE. */ + public static long integerInRange(JsonValue v, long min, long max) { + BigInteger b = integer(v); + if (b.compareTo(BigInteger.valueOf(min)) < 0 || b.compareTo(BigInteger.valueOf(max)) > 0) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_RANGE); + } + return b.longValueExact(); + } + + // --- string constraints --- + + public static int utf8Len(String s) { + return s.getBytes(StandardCharsets.UTF_8).length; + } + + /** Enforce a maximum UTF-8 byte length; over-cap is E_SCHEMA_FIELD_LENGTH. */ + public static void maxBytes(String s, int max) { + if (utf8Len(s) > max) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + } + + /** Reject control characters U+0000..U+001F and U+007F; optionally allow line feed. */ + public static void noControlChars(String s, boolean allowLineFeed) { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\n' && allowLineFeed) { + continue; + } + if (c < 0x20 || c == 0x7F) { + throw syntax(); + } + } + } + + /** Require NFC for a user-visible text field (section 04); non-NFC is E_SCHEMA_FIELD_SYNTAX. */ + public static void requireNfc(String s) { + if (!Normalizer.isNormalized(s, Normalizer.Form.NFC)) { + throw syntax(); + } + } + + /** Slug syntax: [a-z0-9_-], begins with [a-z0-9], 1..maxLen chars (section 03, section 07). */ + public static void slug(String s, int maxLen) { + if (s.isEmpty() || s.length() > maxLen) { + throw syntax(); + } + char first = s.charAt(0); + if (!((first >= 'a' && first <= 'z') || (first >= '0' && first <= '9'))) { + throw syntax(); + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + boolean ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; + if (!ok) { + throw syntax(); + } + } + } + + /** Base64url field of an exact decoded byte length; alphabet/length/canonical violations are E_SCHEMA_FIELD_SYNTAX. */ + public static byte[] base64url(String s, int expectedBytes) { + try { + return Base64Url.decode(s, expectedBytes); + } catch (Base64Url.InvalidBase64Url e) { + throw syntax(); + } + } + + /** + * RFC 3339 UTC timestamp in the only permitted form + * {@code YYYY-MM-DDTHH:MM:SSZ}: integer seconds, no offset, no fraction, no + * leap second. A lexically invalid timestamp is E_SCHEMA_FIELD_SYNTAX. + */ + public static void rfc3339(String s) { + if (!Rfc3339.isValid(s)) { + throw syntax(); + } + } + + /** A path field per section 02 path syntax; violations are E_SCHEMA_FIELD_SYNTAX. */ + public static void path(String s) { + if (!Paths.isValidContentPath(s)) { + throw syntax(); + } + } + + /** Require the value to be one of an enumerated set; otherwise E_SCHEMA_ENUM_VIOLATION. */ + public static void inEnum(String value, Set allowed) { + if (!allowed.contains(value)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + } + + // --- shared throwers --- + + public static RejectException type() { + return new RejectException(DiagnosticCode.E_SCHEMA_FIELD_TYPE); + } + + public static RejectException syntax() { + return new RejectException(DiagnosticCode.E_SCHEMA_FIELD_SYNTAX); + } +} diff --git a/src/main/java/org/entangled/schema/Inline.java b/src/main/java/org/entangled/schema/Inline.java new file mode 100644 index 0000000..892e6bf --- /dev/null +++ b/src/main/java/org/entangled/schema/Inline.java @@ -0,0 +1,193 @@ +package org.entangled.schema; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.entangled.DiagnosticCode; +import org.entangled.RejectException; +import org.entangled.json.JsonValue; + +/** + * Inline content arrays and the link-target schema, section 03. + * + *

An inline array holds {@code text} and {@code link} elements; both carry + * {@code kind}, {@code value} (a UTF-8 visible string), and {@code marks} (zero + * or more of bold/italic/code/strikethrough, no duplicates). A {@code link} + * element additionally carries {@code target}. Inline {@code value} strings are + * NFC, contain no control characters, and contain no line feed. + */ +public final class Inline { + + private static final Set MARKS = Set.of("bold", "italic", "code", "strikethrough"); + private static final Set TARGET_KINDS = Set.of("same_site", "entangled", "carrier", "citation"); + + private static final int MAX_ELEMENTS = 256; + private static final int MAX_VALUE_BYTES = 2048; + + private Inline() { + } + + /** + * Validate an inline content array and return the total UTF-8 byte count of + * its {@code value} strings (callers enforce the per-block aggregate cap). + * + * @param allowLinks whether inline {@code link} elements are permitted here + * ({@code link.label} and {@code submit_form.label} forbid them) + * @param requireNonEmpty whether the array must contain at least one element + */ + public static int validate(JsonValue v, boolean allowLinks, boolean requireNonEmpty) { + List elements = Fields.arr(v).elements(); + if (requireNonEmpty && elements.isEmpty()) { + throw Fields.syntax(); + } + if (elements.size() > MAX_ELEMENTS) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + int totalBytes = 0; + for (JsonValue elemValue : elements) { + totalBytes += validateElement(elemValue, allowLinks); + } + return totalBytes; + } + + private static int validateElement(JsonValue elemValue, boolean allowLinks) { + JsonValue.Obj e = Fields.obj(elemValue); + String kind = Fields.str(require(e, "kind")); + return switch (kind) { + case "text" -> { + Closed.check(e, Set.of("kind", "value", "marks"), Set.of("kind", "value", "marks")); + String value = validateValue(Fields.str(e.get("value"))); + validateMarks(e.get("marks")); + yield Fields.utf8Len(value); + } + case "link" -> { + if (!allowLinks) { + // A link element where only text is permitted is a not-permitted + // inline element; treat as an enum violation on the element kind. + throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + Closed.check(e, Set.of("kind", "value", "marks", "target"), + Set.of("kind", "value", "marks", "target")); + String value = validateValue(Fields.str(e.get("value"))); + validateMarks(e.get("marks")); + validateTarget(e.get("target")); + yield Fields.utf8Len(value); + } + default -> throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + }; + } + + private static String validateValue(String value) { + if (Fields.utf8Len(value) > MAX_VALUE_BYTES) { + throw new RejectException(DiagnosticCode.E_SCHEMA_FIELD_LENGTH); + } + // Inline value: no control chars, no line feed (section 03), NFC (section 04). + Fields.noControlChars(value, false); + Fields.requireNfc(value); + return value; + } + + private static void validateMarks(JsonValue marksValue) { + List marks = Fields.arr(marksValue).elements(); + Set seen = new HashSet<>(); + for (JsonValue m : marks) { + String mark = Fields.str(m); + if (!MARKS.contains(mark)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + if (!seen.add(mark)) { + throw new RejectException(DiagnosticCode.E_SCHEMA_DUPLICATE_ENTRY); + } + } + } + + /** Validate a link target object (shared by inline links and link blocks). */ + public static void validateTarget(JsonValue targetValue) { + JsonValue.Obj t = Fields.obj(targetValue); + String kind = Fields.str(require(t, "kind")); + switch (kind) { + case "same_site" -> { + Closed.check(t, Set.of("kind", "path"), Set.of("kind", "path")); + Fields.path(Fields.str(t.get("path"))); + } + case "entangled" -> { + Closed.check(t, Set.of("kind", "carrier", "address", "path", "expected_publisher_pubkey"), + Set.of("kind", "carrier", "address", "path")); + requireTorV3(Fields.str(t.get("carrier"))); + // address: a 56-char onion + .onion; validated structurally here. + validateOnionAddress(Fields.str(t.get("address"))); + Fields.path(Fields.str(t.get("path"))); + if (t.has("expected_publisher_pubkey")) { + Fields.base64url(Fields.str(t.get("expected_publisher_pubkey")), 32); + } + } + case "carrier" -> { + Closed.check(t, Set.of("kind", "carrier", "url"), Set.of("kind", "carrier", "url")); + requireTorV3(Fields.str(t.get("carrier"))); + validateCarrierUrl(Fields.str(t.get("url"))); + } + case "citation" -> { + Closed.check(t, Set.of("kind", "url"), Set.of("kind", "url")); + validateCitationUrl(Fields.str(t.get("url"))); + } + default -> throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + } + + private static void requireTorV3(String carrier) { + // Section 03: a non-"tor-v3" carrier on a link target is rejected like a + // non-tor-v3 manifest carrier; the value is a syntactically valid string + // outside the permitted set, hence an enum violation. + if (!carrier.equals("tor-v3")) { + throw new RejectException(DiagnosticCode.E_SCHEMA_ENUM_VIOLATION); + } + } + + private static void validateOnionAddress(String address) { + if (!address.endsWith(".onion")) { + throw Fields.syntax(); + } + String body = address.substring(0, address.length() - ".onion".length()); + if (body.length() != 56) { + throw Fields.syntax(); + } + for (int i = 0; i < body.length(); i++) { + char c = body.charAt(i); + boolean ok = (c >= 'a' && c <= 'z') || (c >= '2' && c <= '7'); + if (!ok) { + throw Fields.syntax(); + } + } + } + + private static void validateCarrierUrl(String url) { + if (!url.startsWith("http://") || Fields.utf8Len(url) > 1024) { + throw Fields.syntax(); + } + noControlOrSpace(url); + } + + private static void validateCitationUrl(String url) { + if (!url.startsWith("https://") || Fields.utf8Len(url) > 1024) { + throw Fields.syntax(); + } + noControlOrSpace(url); + } + + private static void noControlOrSpace(String url) { + for (int i = 0; i < url.length(); i++) { + char c = url.charAt(i); + if (c < 0x20 || c == 0x7F) { + throw Fields.syntax(); + } + } + } + + static JsonValue require(JsonValue.Obj obj, String key) { + JsonValue v = obj.get(key); + if (v == null) { + throw new RejectException(DiagnosticCode.E_SCHEMA_REQUIRED_FIELD); + } + return v; + } +} diff --git a/src/main/java/org/entangled/schema/Paths.java b/src/main/java/org/entangled/schema/Paths.java new file mode 100644 index 0000000..abb994d --- /dev/null +++ b/src/main/java/org/entangled/schema/Paths.java @@ -0,0 +1,57 @@ +package org.entangled.schema; + +/** + * Content/transaction/image path syntax, section 02. + * + *

A valid path: + *

    + *
  • begins with {@code /};
  • + *
  • contains only ASCII characters in {@code [A-Za-z0-9._~/-]};
  • + *
  • contains no consecutive {@code /};
  • + *
  • contains no {@code .} or {@code ..} path segment;
  • + *
  • contains no query string, fragment, scheme, or host (these are excluded + * by the character class plus the leading-slash rule);
  • + *
  • is not {@code /manifest.json} or {@code /content_index.json} (reserved);
  • + *
  • does not exceed 256 ASCII characters.
  • + *
+ */ +public final class Paths { + + public static final String RESERVED_MANIFEST = "/manifest.json"; + public static final String RESERVED_CONTENT_INDEX = "/content_index.json"; + + private Paths() { + } + + public static boolean isValidContentPath(String p) { + if (p.isEmpty() || p.charAt(0) != '/' || p.length() > 256) { + return false; + } + if (p.equals(RESERVED_MANIFEST) || p.equals(RESERVED_CONTENT_INDEX)) { + return false; + } + for (int i = 0; i < p.length(); i++) { + char c = p.charAt(i); + boolean ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '.' || c == '_' || c == '~' || c == '/' || c == '-'; + if (!ok) { + return false; + } + } + // No consecutive slashes. + if (p.contains("//")) { + return false; + } + // No "." or ".." segments. Split on '/'; the leading slash yields an + // empty first segment, which is fine (and any other empty segment would + // be a "//" already rejected above). + String[] segments = p.split("/", -1); + for (String seg : segments) { + if (seg.equals(".") || seg.equals("..")) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/org/entangled/schema/Rfc3339.java b/src/main/java/org/entangled/schema/Rfc3339.java new file mode 100644 index 0000000..c4066ac --- /dev/null +++ b/src/main/java/org/entangled/schema/Rfc3339.java @@ -0,0 +1,52 @@ +package org.entangled.schema; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * The single RFC 3339 timestamp form Entangled permits (section 02, section 06, + * section 08): {@code YYYY-MM-DDTHH:MM:SSZ}. + * + *

Integer seconds only; no numeric UTC offset, no fractional seconds, no + * leap-second values, uppercase {@code T} and {@code Z}. The string is validated + * both lexically (exact shape) and as a real calendar instant. + */ +public final class Rfc3339 { + + // Strict shape: 4-2-2 'T' 2:2:2 'Z'. Lexical gate before calendar parsing. + private static final java.util.regex.Pattern SHAPE = + java.util.regex.Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); + + private static final DateTimeFormatter PARSER = + DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC); + + private Rfc3339() { + } + + /** True iff {@code s} is exactly the permitted form and a real instant. */ + public static boolean isValid(String s) { + if (!SHAPE.matcher(s).matches()) { + return false; + } + // A "60" seconds field is a leap second and is forbidden; ResolverStyle + // STRICT rejects it along with impossible dates. + try { + parse(s); + return true; + } catch (DateTimeParseException e) { + return false; + } + } + + /** Parse a validated timestamp to an instant in epoch seconds (UTC). */ + public static long epochSeconds(String s) { + return parse(s).toEpochSecond(); + } + + private static OffsetDateTime parse(String s) { + return OffsetDateTime.parse(s, + PARSER.withResolverStyle(java.time.format.ResolverStyle.STRICT)); + } +} diff --git a/src/main/resources/org/entangled/crypto/bip39_english.txt b/src/main/resources/org/entangled/crypto/bip39_english.txt new file mode 100644 index 0000000..942040e --- /dev/null +++ b/src/main/resources/org/entangled/crypto/bip39_english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo diff --git a/src/test/java/org/entangled/ConformanceTest.java b/src/test/java/org/entangled/ConformanceTest.java new file mode 100644 index 0000000..2f6d4ac --- /dev/null +++ b/src/test/java/org/entangled/ConformanceTest.java @@ -0,0 +1,157 @@ +package org.entangled; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.entangled.json.JsonParser; +import org.entangled.json.JsonValue; +import org.entangled.pipeline.Context; +import org.entangled.pipeline.Pipeline; +import org.entangled.schema.Rfc3339; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * The normative conformance suite (corpus/README.md): drive every corpus vector + * through the validation pipeline and assert the implementation's outcome + * against the recorded {@code expected} verdict, diagnostic code, and structured + * details. This is the code-vs-corpus verification; success is 62/62. + * + *

The clock is mocked to {@code corpus.json.clock_now}. Each vector's + * {@code context} block is mapped onto a {@link Context}: fetched origin/path, + * submit path and body, the authorized runtime key, the seeded publisher history + * ({@code previously_verified} / {@code previously_verified_history}), and the + * successor manifest for migration scenarios. + */ +class ConformanceTest { + + private static final java.nio.file.Path ROOT = CorpusFiles.ROOT; + + @TestFactory + List corpusVectors() { + JsonValue.Obj corpus = (JsonValue.Obj) JsonParser.parse( + new String(CorpusFiles.bytes("corpus.json"), StandardCharsets.UTF_8)); + long clockNow = Rfc3339.epochSeconds(str(corpus.get("clock_now"))); + + List vectors = ((JsonValue.Arr) corpus.get("vectors")).elements(); + List tests = new ArrayList<>(); + for (JsonValue vEntry : vectors) { + JsonValue.Obj vector = (JsonValue.Obj) vEntry; + String id = str(vector.get("id")); + tests.add(DynamicTest.dynamicTest(id, () -> runVector(vector, clockNow))); + } + // Guard against silently testing fewer vectors than the corpus declares. + assertEquals(62, tests.size(), "corpus vector count"); + return tests; + } + + private void runVector(JsonValue.Obj vector, long clockNow) { + String id = str(vector.get("id")); + String inputRel = str(vector.get("input")); + byte[] body = CorpusFiles.bytes(inputRel); + + Context ctx = new Context(clockNow); + ctx.expectedKind = switch (str(vector.get("kind"))) { + case "manifest" -> org.entangled.pipeline.Stage4Kind.Kind.MANIFEST; + case "content" -> org.entangled.pipeline.Stage4Kind.Kind.CONTENT; + case "transaction" -> org.entangled.pipeline.Stage4Kind.Kind.TRANSACTION; + default -> null; + }; + JsonValue ctxValue = vector.get("context"); + if (ctxValue instanceof JsonValue.Obj c) { + applyContext(c, ctx); + } + + Verdict verdict = new Pipeline(ctx).run(body); + + JsonValue.Obj expected = (JsonValue.Obj) vector.get("expected"); + String expectedVerdict = str(expected.get("verdict")); + + if (expectedVerdict.equals("accept")) { + if (!verdict.isAccepted()) { + fail(id + ": expected accept but got " + verdict); + } + return; + } + + // reject + assertTrue(!verdict.isAccepted(), id + ": expected reject but accepted"); + String expectedCode = str(expected.get("diagnostic")); + assertEquals(expectedCode, verdict.diagnostic().code().name(), id + ": diagnostic code"); + + if (expected.has("diagnostic_details")) { + Map want = toJavaMap((JsonValue.Obj) expected.get("diagnostic_details")); + Map got = verdict.diagnostic().details(); + assertEquals(want, got, id + ": diagnostic details"); + } + } + + private void applyContext(JsonValue.Obj c, Context ctx) { + if (c.has("fetched_origin_address")) { + ctx.fetchedOriginAddress = str(c.get("fetched_origin_address")); + } + if (c.has("fetched_path")) { + ctx.fetchedPath = str(c.get("fetched_path")); + } + if (c.has("submit_path")) { + ctx.submitPath = str(c.get("submit_path")); + } + if (c.has("expected_runtime_pubkey")) { + ctx.expectedRuntimePubkey = str(c.get("expected_runtime_pubkey")); + } + if (c.has("submit_body_path")) { + ctx.submitBody = CorpusFiles.bytes(str(c.get("submit_body_path"))); + } + if (c.has("successor_origin_address")) { + ctx.successorOriginAddress = str(c.get("successor_origin_address")); + } + if (c.has("successor_manifest_path")) { + ctx.successorManifest = CorpusFiles.bytes(str(c.get("successor_manifest_path"))); + } + if (c.has("previously_verified")) { + ctx.publisherHistory.add(CorpusFiles.bytes(str(c.get("previously_verified")))); + } + if (c.has("previously_verified_history")) { + for (JsonValue p : ((JsonValue.Arr) c.get("previously_verified_history")).elements()) { + ctx.publisherHistory.add(CorpusFiles.bytes(str(p))); + } + } + } + + // --- JSON helpers (using the implementation's own parser) --- + + private static String str(JsonValue v) { + return ((JsonValue.Str) v).value(); + } + + /** Convert a parsed details object to the Java map shape the pipeline produces. */ + private static Map toJavaMap(JsonValue.Obj obj) { + Map out = new LinkedHashMap<>(); + for (Map.Entry e : obj.members().entrySet()) { + out.put(e.getKey(), toJava(e.getValue())); + } + return out; + } + + private static Object toJava(JsonValue v) { + if (v instanceof JsonValue.Str s) { + return s.value(); + } + if (v instanceof JsonValue.Num n) { + // Corpus details integers compare against Long values the pipeline emits. + BigInteger b = n.value(); + return b == null ? n.raw() : b.longValueExact(); + } + if (v instanceof JsonValue.Bool b) { + return b.value(); + } + return v.toString(); + } +} diff --git a/src/test/java/org/entangled/CorpusFiles.java b/src/test/java/org/entangled/CorpusFiles.java new file mode 100644 index 0000000..81fa020 --- /dev/null +++ b/src/test/java/org/entangled/CorpusFiles.java @@ -0,0 +1,44 @@ +package org.entangled; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Test helper: read raw corpus bytes from the checked-in copy under + * {@code src/test/resources/corpus}. Bytes are read with no normalization and + * no transcoding, as the corpus harness contract requires (corpus/README.md). + */ +final class CorpusFiles { + + static final Path ROOT = Paths.get("src", "test", "resources", "corpus"); + + private CorpusFiles() { + } + + static byte[] bytes(String relative) { + try { + return Files.readAllBytes(ROOT.resolve(relative)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + static byte[] vectorInput(String vectorId) { + return bytes("vectors/" + vectorId + "/input.json"); + } + + static byte[] classpath(String resource) { + try (InputStream in = CorpusFiles.class.getResourceAsStream(resource)) { + if (in == null) { + throw new IllegalArgumentException("missing resource: " + resource); + } + return in.readAllBytes(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/test/java/org/entangled/CryptoTest.java b/src/test/java/org/entangled/CryptoTest.java new file mode 100644 index 0000000..c0e1a1a --- /dev/null +++ b/src/test/java/org/entangled/CryptoTest.java @@ -0,0 +1,104 @@ +package org.entangled; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import org.entangled.crypto.Base64Url; +import org.entangled.crypto.Bip39Pip; +import org.entangled.crypto.Ed25519; +import org.entangled.crypto.TorV3Address; +import org.entangled.json.Jcs; +import org.entangled.json.JsonParser; +import org.entangled.json.JsonValue; +import org.junit.jupiter.api.Test; + +/** + * Crypto primitives, anchored on the fixture material in corpus/keys.json and on + * a real corpus signature (vector 001), so the whole base64url + JCS + Ed25519 + * stack is exercised against known-good bytes derived from the spec corpus. + */ +class CryptoTest { + + private static final String PUBLISHER_PUB_B64U = "moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc"; + private static final String PUBLISHER_PIP = + "once grain trumpet rookie common appear canyon blur eye guide small betray " + + "tissue depth mutual swift admit text level practice hunt accuse hobby unusual"; + private static final String ORIGIN_PUB_B64U = "Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"; + private static final String ORIGIN_ONION = + "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion"; + private static final String ORIGIN2_PUB_B64U = "Ko4-lXCoSgOUc_2Dt2Bi9NcyVyUN3GYgEKMJkgPQtb8"; + private static final String ORIGIN2_ONION = + "fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion"; + + @Test + void base64urlStrictDecode() { + // A 43-char base64url decodes to 32 bytes. + byte[] pub = Base64Url.decode(PUBLISHER_PUB_B64U, 32); + assertEquals(32, pub.length); + // Wrong declared length is rejected. + assertThrows(Base64Url.InvalidBase64Url.class, () -> Base64Url.decode(PUBLISHER_PUB_B64U, 64)); + // Padding, standard alphabet, and whitespace are rejected. + assertThrows(Base64Url.InvalidBase64Url.class, () -> Base64Url.decode("AAAA====", 4)); + assertThrows(Base64Url.InvalidBase64Url.class, () -> Base64Url.decode("ab+/" + "AAAA", 6)); + assertThrows(Base64Url.InvalidBase64Url.class, () -> Base64Url.decode("AA AA", 3)); + } + + @Test + void pipMatchesKeysJson() { + byte[] pub = Base64Url.decode(PUBLISHER_PUB_B64U, 32); + assertEquals(PUBLISHER_PIP, Bip39Pip.derive(pub)); + } + + @Test + void onionAddressesDecodeToTheirPubkeys() { + assertArrayEquals(Base64Url.decode(ORIGIN_PUB_B64U, 32), TorV3Address.decodePublicKey(ORIGIN_ONION)); + assertArrayEquals(Base64Url.decode(ORIGIN2_PUB_B64U, 32), TorV3Address.decodePublicKey(ORIGIN2_ONION)); + } + + @Test + void onionAddressChecksumIsChecked() { + // Flip one character: checksum (or structure) must fail. + String tampered = "e" + ORIGIN_ONION.substring(1); + assertThrows(TorV3Address.InvalidOnionAddress.class, () -> TorV3Address.decodePublicKey(tampered)); + } + + @Test + void verifiesRealManifestSignatureVector001() { + byte[] body = CorpusFiles.vectorInput("001-manifest-valid-minimal"); + String text = new String(body, StandardCharsets.UTF_8); + JsonValue.Obj doc = (JsonValue.Obj) JsonParser.parse(text); + + String sigB64u = ((JsonValue.Str) doc.get("sig")).value(); + byte[] sig = Base64Url.decode(sigB64u, 64); + byte[] pub = Base64Url.decode(((JsonValue.Str) doc.get("publisher_pubkey")).value(), 32); + + // signature_input = "ENTANGLED-v1 manifest" || 0x00 || JCS(payload minus sig) + Map members = new LinkedHashMap<>(doc.members()); + members.remove("sig"); + byte[] jcs = Jcs.canonicalize(new JsonValue.Obj(members)); + byte[] input = signatureInput("ENTANGLED-v1 manifest", jcs); + + assertTrue(Ed25519.verify(pub, sig, input), "vector 001 manifest signature must verify"); + + // A one-bit flip in the message must fail. + byte[] tampered = input.clone(); + tampered[tampered.length - 1] ^= 0x01; + assertFalse(Ed25519.verify(pub, sig, tampered)); + } + + private static byte[] signatureInput(String context, byte[] jcs) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] ctx = context.getBytes(StandardCharsets.US_ASCII); + out.writeBytes(ctx); + out.write(0x00); + out.writeBytes(jcs); + return out.toByteArray(); + } +} diff --git a/src/test/java/org/entangled/JcsTest.java b/src/test/java/org/entangled/JcsTest.java new file mode 100644 index 0000000..2b19d6c --- /dev/null +++ b/src/test/java/org/entangled/JcsTest.java @@ -0,0 +1,74 @@ +package org.entangled; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import org.entangled.json.Jcs; +import org.entangled.json.JsonParser; +import org.entangled.json.JsonValue; +import org.junit.jupiter.api.Test; + +/** + * JCS canonicalization, anchored on the section 04 test vector and its exact + * 72-byte expected output. + */ +class JcsTest { + + @Test + void section04TestVector() { + String input = "{\n \"kind\": \"content\",\n \"spec_version\": \"1.0\"," + + "\n \"value\": \"hello world\",\n \"count\": 42\n}"; + String expected = "{\"count\":42,\"kind\":\"content\",\"spec_version\":\"1.0\",\"value\":\"hello world\"}"; + + JsonValue parsed = JsonParser.parse(input); + String canonical = Jcs.canonicalString(parsed); + + assertEquals(expected, canonical); + + byte[] expectedBytes = { + 0x7B, 0x22, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x22, 0x3A, 0x34, 0x32, 0x2C, 0x22, 0x6B, 0x69, 0x6E, + 0x64, 0x22, 0x3A, 0x22, 0x63, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x22, 0x2C, 0x22, 0x73, 0x70, + 0x65, 0x63, 0x5F, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x22, 0x31, 0x2E, 0x30, + 0x22, 0x2C, 0x22, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x22, 0x3A, 0x22, 0x68, 0x65, 0x6C, 0x6C, 0x6F, + 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x22, 0x7D + }; + assertEquals(72, expectedBytes.length); + assertArrayEquals(expectedBytes, Jcs.canonicalize(parsed)); + } + + @Test + void largeIntegerKeepsExactDigits() { + // Above 2^53, an Entangled integer keeps its exact decimal digits and is + // not routed through binary64 (section 04 integer serialization override). + String input = "{\"seq\":9007199254740993}"; + JsonValue parsed = JsonParser.parse(input); + assertEquals("{\"seq\":9007199254740993}", Jcs.canonicalString(parsed)); + } + + @Test + void minimalStringEscaping() { + // Only ", \\, and control chars are escaped; non-ASCII is raw UTF-8. + String input = "{\"k\":\"a\\\"b\\\\c\\nd\\u0001e\"}"; + JsonValue parsed = JsonParser.parse(input); + String canonical = Jcs.canonicalString(parsed); + assertEquals("{\"k\":\"a\\\"b\\\\c\\nd\\u0001e\"}", canonical); + } + + @Test + void nonAsciiEmittedRaw() { + // A precomposed e-acute (U+00E9) is emitted as its raw UTF-8 bytes + // (0xC3 0xA9), not as a \\uXXXX escape. This matters for signatures over + // documents with non-ASCII text (for example canary.statement), which + // the ASCII-only section 04 vector does not exercise. The U+00E9 is + // written as a \\u00e9 Java char literal for an unambiguous expectation. + String input = "{\"k\":\"caf\u00e9\"}"; + JsonValue parsed = JsonParser.parse(input); + byte[] canonical = Jcs.canonicalize(parsed); + byte[] expected = ("{\"k\":\"caf\u00e9\"}").getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expected, canonical); + // Confirm the e-acute is the two raw UTF-8 bytes, not a \\u escape. + assertEquals((byte) 0xC3, canonical[canonical.length - 4]); + assertEquals((byte) 0xA9, canonical[canonical.length - 3]); + } +} diff --git a/src/test/java/org/entangled/JsonLayerTest.java b/src/test/java/org/entangled/JsonLayerTest.java new file mode 100644 index 0000000..7518ac2 --- /dev/null +++ b/src/test/java/org/entangled/JsonLayerTest.java @@ -0,0 +1,117 @@ +package org.entangled; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.entangled.json.JsonParser; +import org.entangled.json.JsonValue; +import org.entangled.pipeline.Stage2Input; +import org.junit.jupiter.api.Test; + +/** + * Stage 2 (input) and Stage 3 (parse) behavior, driven by the corpus vectors + * whose first failing stage is one of those stages. These do not exercise the + * full pipeline; they verify that the byte/parse layer raises the right + * diagnostic before any later stage runs. + */ +class JsonLayerTest { + + private static int capForManifest() { + return Stage2Input.MANIFEST_BYTE_CAP; + } + + private static int capForContent() { + return Stage2Input.CONTENT_BYTE_CAP; + } + + private RejectException rejectOf(String vectorId, int cap) { + byte[] body = CorpusFiles.vectorInput(vectorId); + return assertThrows(RejectException.class, () -> { + String text = Stage2Input.validateAndDecode(body, cap); + JsonParser.parse(text); + }); + } + + @Test + void inputBomVector100() { + assertEquals(DiagnosticCode.E_INPUT_BOM, + rejectOf("100-input-bom", capForManifest()).diagnostic().code()); + } + + @Test + void inputBadUtf8Vector101() { + assertEquals(DiagnosticCode.E_INPUT_UTF8, + rejectOf("101-input-bad-utf8", capForManifest()).diagnostic().code()); + } + + @Test + void inputByteCapVector102() { + assertEquals(DiagnosticCode.E_INPUT_BYTE_CAP, + rejectOf("102-input-byte-cap", capForManifest()).diagnostic().code()); + } + + @Test + void duplicateKeysVector110() { + assertEquals(DiagnosticCode.E_PARSE_DUPLICATE_KEY, + rejectOf("110-parse-duplicate-keys", capForContent()).diagnostic().code()); + } + + @Test + void nestingDepthVector111() { + assertEquals(DiagnosticCode.E_PARSE_NESTING_DEPTH, + rejectOf("111-parse-nesting-depth", capForManifest()).diagnostic().code()); + } + + @Test + void stringLengthVector112() { + assertEquals(DiagnosticCode.E_PARSE_STRING_LENGTH, + rejectOf("112-parse-string-length", capForContent()).diagnostic().code()); + } + + @Test + void arrayLengthVector113() { + assertEquals(DiagnosticCode.E_PARSE_ARRAY_LENGTH, + rejectOf("113-parse-array-length", capForContent()).diagnostic().code()); + } + + @Test + void objectKeysVector114() { + assertEquals(DiagnosticCode.E_PARSE_OBJECT_KEYS, + rejectOf("114-parse-object-keys", capForContent()).diagnostic().code()); + } + + @Test + void malformedJsonVector115() { + assertEquals(DiagnosticCode.E_PARSE_JSON, + rejectOf("115-parse-json-malformed", capForManifest()).diagnostic().code()); + } + + @Test + void integerGrammarClassification() { + // Conforming integers (well-formed JSON and matching the section 04 grammar). + assertTrue(((JsonValue.Num) JsonParser.parse("0")).conformingInteger()); + assertTrue(((JsonValue.Num) JsonParser.parse("42")).conformingInteger()); + assertTrue(((JsonValue.Num) JsonParser.parse("9223372036854775807")).conformingInteger()); + // Non-conforming but well-formed JSON: float, exponent, overflow. These + // parse (so a later stage can read them) but are flagged non-integer for + // Stage 5 (E_SCHEMA_NON_INTEGER): vectors 140, 141, 142. + assertFalse(((JsonValue.Num) JsonParser.parse("42.0")).conformingInteger()); + assertFalse(((JsonValue.Num) JsonParser.parse("4.2e1")).conformingInteger()); + assertFalse(((JsonValue.Num) JsonParser.parse("9223372036854775808")).conformingInteger()); + // A leading zero followed by a digit (01) and a bare sign with nothing + // are not valid JSON at all, so they are E_PARSE_JSON, not a Stage 5 + // integer-grammar failure. No corpus vector exercises leading-zero, + // consistent with this being a JSON-level malformation. + assertThrows(RejectException.class, () -> JsonParser.parse("01")); + } + + @Test + void wellFormedNumbersParseEvenWhenNonInteger() { + // A float is valid JSON and must parse; the E_SCHEMA_NON_INTEGER judgment + // happens at Stage 5, not at parse time. + JsonValue v = JsonParser.parse("{\"x\":1.5}"); + assertTrue(v instanceof JsonValue.Obj); + } +} diff --git a/src/test/java/org/entangled/SchemaStageTest.java b/src/test/java/org/entangled/SchemaStageTest.java new file mode 100644 index 0000000..203d5d5 --- /dev/null +++ b/src/test/java/org/entangled/SchemaStageTest.java @@ -0,0 +1,112 @@ +package org.entangled; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.entangled.json.JsonParser; +import org.entangled.json.JsonValue; +import org.entangled.pipeline.Stage2Input; +import org.entangled.pipeline.Stage4Kind; +import org.entangled.schema.DocumentSchema; +import org.entangled.schema.Rfc3339; +import org.junit.jupiter.api.Test; + +/** + * Stage 4 (kind) and Stage 5 (closed schema, including cross-field semantic + * checks) driven by the corpus vectors whose first failing stage is one of + * those, plus the positive vectors which must pass Stage 5 cleanly. + * + *

This does not run signature, canary, or binding stages; it isolates the + * schema layer ahead of the full pipeline. + */ +class SchemaStageTest { + + private static final long CLOCK_NOW = Rfc3339.epochSeconds("2026-05-07T00:01:00Z"); + + private DiagnosticCode rejectCode(String vectorId, int cap) { + RejectException ex = assertThrows(RejectException.class, () -> runStage5(vectorId, cap)); + return ex.diagnostic().code(); + } + + private void runStage5(String vectorId, int cap) { + byte[] body = CorpusFiles.vectorInput(vectorId); + String text = Stage2Input.validateAndDecode(body, cap); + JsonValue root = JsonParser.parse(text); + Stage4Kind.Kind kind = Stage4Kind.discriminate(root); + JsonValue.Obj doc = (JsonValue.Obj) root; + switch (kind) { + case MANIFEST -> DocumentSchema.validateManifest(doc, CLOCK_NOW); + case CONTENT -> DocumentSchema.validateContent(doc); + case TRANSACTION -> DocumentSchema.validateTransaction(doc); + } + } + + // --- Stage 4 --- + + @Test + void specVersionWrong120() { + assertEquals(DiagnosticCode.E_KIND_SPEC_VERSION, + rejectCode("120-spec-version-wrong", Stage2Input.MANIFEST_BYTE_CAP)); + } + + @Test + void kindUnknown121() { + assertEquals(DiagnosticCode.E_KIND_UNKNOWN, + rejectCode("121-kind-unknown", Stage2Input.MANIFEST_BYTE_CAP)); + } + + @Test + void kindMissingFields122() { + assertEquals(DiagnosticCode.E_KIND_MISSING_FIELDS, + rejectCode("122-kind-missing-fields", Stage2Input.MANIFEST_BYTE_CAP)); + } + + // --- Stage 5 schema --- + + @Test + void schemaVectors() { + record Case(String id, int cap, DiagnosticCode code) { + } + Case[] cases = { + new Case("130-schema-unknown-field", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_UNKNOWN_FIELD), + new Case("131-schema-missing-required", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_REQUIRED_FIELD), + new Case("132-schema-null-value", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_NULL_VALUE), + new Case("133-schema-block-kind-unknown", Stage2Input.CONTENT_BYTE_CAP, DiagnosticCode.E_SCHEMA_ENUM_VIOLATION), + new Case("134-schema-field-type", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_TYPE), + new Case("135-schema-field-range", Stage2Input.CONTENT_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_RANGE), + new Case("137-schema-duplicate-entry", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_DUPLICATE_ENTRY), + new Case("138-schema-malformed-unicode", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_MALFORMED_UNICODE), + new Case("139-schema-field-length", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_LENGTH), + new Case("140-numeric-float", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_NON_INTEGER), + new Case("141-numeric-exponent", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_NON_INTEGER), + new Case("142-numeric-overflow", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_NON_INTEGER), + new Case("143-submit-budget-state-overflow", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SUBMIT_BUDGET), + new Case("151-sig-syntax-length", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_SYNTAX), + new Case("160-base64url-padded", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_SYNTAX), + new Case("161-base64url-standard-alphabet", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_SYNTAX), + new Case("162-base64url-whitespace", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_SYNTAX), + new Case("171-bind-reserved-manifest-path", Stage2Input.CONTENT_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_SYNTAX), + new Case("176-origin-invalid", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_ORIGIN_INVALID), + new Case("177-origin-invalid-beyond-5y", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_ORIGIN_INVALID), + new Case("178-manifest-updated-future-skew", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_SYNTAX), + new Case("190-unicode-nfd-statement", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_SYNTAX), + new Case("191-unicode-nfd-freshness-proof", Stage2Input.MANIFEST_BYTE_CAP, DiagnosticCode.E_SCHEMA_FIELD_SYNTAX), + }; + for (Case c : cases) { + assertEquals(c.code(), rejectCode(c.id(), c.cap()), "vector " + c.id()); + } + } + + @Test + void positiveVectorsPassSchema() { + // The accept vectors must pass Stage 5 without throwing. + assertDoesNotThrow(() -> runStage5("001-manifest-valid-minimal", Stage2Input.MANIFEST_BYTE_CAP)); + assertDoesNotThrow(() -> runStage5("002-manifest-valid-state-policy", Stage2Input.MANIFEST_BYTE_CAP)); + assertDoesNotThrow(() -> runStage5("003-content-valid-minimal", Stage2Input.CONTENT_BYTE_CAP)); + assertDoesNotThrow(() -> runStage5("004-content-valid-blocks-showcase", Stage2Input.CONTENT_BYTE_CAP)); + assertDoesNotThrow(() -> runStage5("005-transaction-valid-minimal", Stage2Input.TRANSACTION_BYTE_CAP)); + assertDoesNotThrow(() -> runStage5("006-manifest-valid-not-after", Stage2Input.MANIFEST_BYTE_CAP)); + assertDoesNotThrow(() -> runStage5("007-content-valid-large-seq", Stage2Input.CONTENT_BYTE_CAP)); + } +} diff --git a/src/test/java/org/entangled/SmokeTest.java b/src/test/java/org/entangled/SmokeTest.java new file mode 100644 index 0000000..f776297 --- /dev/null +++ b/src/test/java/org/entangled/SmokeTest.java @@ -0,0 +1,14 @@ +package org.entangled; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class SmokeTest { + + @Test + void specConstants() { + assertEquals("1.0", Entangled.SPEC_VERSION); + assertEquals("1.0-rc.27", Entangled.SPEC_REVISION); + } +} diff --git a/src/test/resources/corpus/README.md b/src/test/resources/corpus/README.md new file mode 100644 index 0000000..7eefc45 --- /dev/null +++ b/src/test/resources/corpus/README.md @@ -0,0 +1,115 @@ +# Entangled v1.0 conformance corpus + +Test vectors for Entangled v1.0 protocol implementations. + +## Status + +This corpus is normative: a v1.0-conforming implementation MUST agree with the verdicts recorded here for each vector. Implementations are encouraged to drive their own conformance test suite from `corpus.json`. + +The corpus is generated deterministically from a fixed set of test seeds. Anyone can reproduce it byte-for-byte by running the generator. + +## Layout + +``` +corpus/ ++-- README.md this file ++-- keys.json public key material derived from fixed test seeds ++-- corpus.json machine-readable index: vector id, expected verdict, etc. ++-- vectors/ +| `-- / +| +-- input.json (or input.bin for non-JSON inputs) +| `-- ... extra files such as submit_body.json, image bytes +`-- tools/ + `-- generate.py deterministic generator +``` + +`corpus.json` is the entry point. Every vector is described by: + +- `id` - stable identifier, prefixed with a numeric category (001-099 positive, 100-199 single-document negative diagnostics organized by pipeline stage, 200-299 multi-document scenarios such as migration; the full per-stage breakdown is in the "Categories of vectors" table below); +- `kind` - `manifest`, `content`, or `transaction` (the kind of the primary input document; multi-document scenarios may carry additional documents in `extra_files`); +- `description` - what the vector exercises; +- `spec_refs` - the spec sections the vector tests; +- `input` - relative path to the input bytes of the primary document; +- `expected.verdict` - `accept` or `reject` (for multi-document scenarios such as migration vectors, the verdict refers to the scenario outcome - e.g., the migration adoption - not necessarily the in-isolation validity of the primary document); +- `expected.diagnostic` - for rejections, the normative §11 diagnostic code; +- `expected.diagnostic_details` - for rejections whose §11 diagnostic carries structured `details` (e.g., `E_MIGRATION_MISMATCH` with `mismatch_field` and `underlying_diagnostic_code`), the expected `details` object the implementation should produce; +- `context` - optional fields needed to apply the vector (fetched path, fetched origin address, prerequisites such as a previously verified manifest, the corresponding submit body for transactions, the address and on-disk path of a successor manifest for migration scenarios, etc.). The `previously_verified` field (single path) seeds the client's publisher history with one prior manifest. The `previously_verified_history` field (array of paths in publication order, oldest first) seeds the client's publisher history with a sequence of prior manifests; used by vectors that exercise rules whose scope extends beyond the immediately preceding manifest (for example, the §08 SHOULD-level runtime-pubkey resurrection check, vector 185); +- `extra_files` - additional files in the vector directory (e.g., `submit_body.json` for transactions, `successor_manifest.json` for migration scenarios). + +The corpus index also carries a top-level `clock_now` field, in RFC 3339 form. Harnesses MUST mock the implementation's wall clock to this value for the duration of the test run. This is required because canary diagnostics depend on `now` and the corpus uses fixed `issued_at` timestamps; without clock mocking, time-dependent vectors are not reproducible. + +## Test keys + +`keys.json` records the test-only Ed25519 keypairs derived from fixed 32-byte seeds. The seeds are public ASCII strings (e.g., `b"ENTANGLED-v1.0-publisher-test01\x00"`); the corresponding private keys are NOT secret. They MUST NOT be used for any deployment. + +Three roles are pre-derived: `publisher` (`K_publisher`), `runtime` (`K_runtime`), `origin` (`K_origin`). A second runtime keypair (`runtime_2`) is provided for tests that need a distinct `K_runtime.pub` (e.g., the equal-`issued_at` conflict vector). A second origin keypair (`origin_2`) is provided for migration scenarios where the announcing and successor manifests bind to different `K_origin` keys; its Tor v3 onion address is also recorded. + +For the `origin` and `origin_2` keypairs, the corresponding Tor v3 onion address is recorded alongside the public key; each address is derived from its public key by the rend-spec-v3 procedure and used for origin-binding in the relevant manifest vectors. + +The `publisher` entry in `keys.json` also carries `pip`: the 24-word Publisher Identity Phrase derived from `publisher.pub_b64u` per §05 (BIP-39 English wordlist over the raw 32-byte public key, with an 8-bit SHA-256 checksum). An implementation that derives PIPs MUST produce the same string for this public key. The wordlist used by `generate.py` is bundled at `tools/bip39_english.txt` and is the canonical BIP-39 English wordlist from the Bitcoin BIPs repository (`bitcoin/bips: bip-0039/english.txt`); its SHA-256 is `2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda`. The `pip` field is omitted when the wordlist file is absent; presence of the field is therefore the indicator that the corpus was regenerated with a verified wordlist in place. + +## Diagnostics + +Negative vectors carry the normative diagnostic code from §11 of the specification. Where multiple stages could in principle detect the violation, the diagnostic listed is the one the spec assigns (or, for parser-detectable cases, the one whose protocol-level meaning matches the violation regardless of detection stage). + +Negative vectors are constructed so that the diagnostic-relevant violation is the only violation in the document at the first failing pipeline stage. After all earlier stages pass cleanly, exactly one diagnostic-relevant violation is intended to be live; later-stage violations may exist in the same document only when they cannot be exercised in isolation (e.g., a placeholder `sig` for a vector whose diagnostic precedes signature verification). This isolation is what makes the expected diagnostic deterministic across conforming implementations: a vector that contains two competing stage-5 violations would permit two equally-conformant rejection codes, defeating the corpus's normative purpose. + +## Running the corpus against an implementation + +The general test harness pattern: + +1. Load `corpus.json`. +2. Set the implementation's wall clock to `corpus.json["clock_now"]` (mock or inject) for the duration of the test run. +3. For each vector: + - read the raw input bytes from `input` (no normalization, no transcoding); + - apply implementation-specific context: e.g., set the "fetched path" to `context.fetched_path` for content documents, set the "previously verified manifest" for canary-conflict vectors, etc.; + - run the input through the implementation's validation pipeline; + - compare the implementation's outcome against `expected`. + +Implementations SHOULD report any vector whose actual outcome diverges from the expected one as a conformance failure. + +## Regenerating + +``` +python3 corpus/tools/generate.py +``` + +Requires Python 3.10+ and the `cryptography` package (for raw Ed25519 RFC 8032 signing). The generator is fully deterministic; output bytes match across runs and across machines. + +## Categories of vectors + +| Range | Category | +|---|---| +| 001-099 | Positive (must be accepted) | +| 100-109 | Stage 2 input checks (BOM, UTF-8, byte cap) | +| 110-119 | Stage 3 JSON parsing (duplicate keys, nesting depth, string length, array length, object keys, malformed JSON) | +| 120-129 | Stage 4 kind discrimination (spec_version, unknown kind, missing required top-level field) | +| 130-139 | Stage 5 schema (unknown field, missing required, null literal, unknown block kind, field type, field range, block not permitted in document kind, duplicate uniqueness-required entry, malformed Unicode, field-specific length cap) | +| 140-142 | Numeric grammar (float, exponent, overflow) | +| 143 | Stage 5 semantic - submit-budget state-policy aggregate overflow | +| 144-149 | (reserved) | +| 150-159 | Stage 6 signature (modified payload, malformed length, non-canonical S, small-order A, non-canonical R, non-canonical A, missing-key context, small-order R) | +| 160-169 | Strict base64url (padding, alphabet, whitespace) | +| 170-179 | Stage 9 binding (path mismatch, reserved path, request_hash, origin binding, origin not_after semantic constraints including both `reason` values, manifest.updated future-skew) | +| 180-189 | Canary (equal `issued_at` conflict, anti-downgrade, interval-bounds violation, issued_at future-skew, runtime-key reuse) | +| 190-199 | Unicode and canonicalization (NFD vs NFC) | +| 200-209 | Migration scenarios (successor_stage9_failure under `E_MIGRATION_MISMATCH`, chain-cycle and announcement-internal successor_key_mismatch under `E_MIGRATION_INVALID`; multi-document scenarios carry the successor manifest in `extra_files`) | + +Coverage relative to the §11 diagnostic code catalog remains partial. Codes not yet covered in this corpus fall into the following groups: + +- **Stage 1 transport** (`E_TRANSPORT_*`, all 13 codes): require an extension of the vector schema to carry expected HTTP response metadata (status code, headers) alongside the body bytes. The pipeline-isolation rule applies normally; only the schema extension is open. +- **Stage 7 trust** (`E_TRUST_MISMATCH`, `E_TRUST_USER_REJECTED`): require multi-manifest scenarios that establish a prior pin and present a different `K_publisher.pub`. +- **Stage 9 binding** sub-codes whose isolation is currently ambiguous: `E_BIND_RESPONSE_PATH`, `E_BIND_REQUEST_ID` (the latter cannot be exercised in isolation from `E_BIND_REQUEST_HASH` because `request_id` is part of the hashed submit body; §10 does not normatively order Stage-9 sub-checks). +- **Stage 9 origin lifecycle**: `E_ORIGIN_EXPIRED` requires a vector whose canary is not Expired at `clock_now` (otherwise Stage 8 `E_CANARY_EXPIRED` is now an error and stops the pipeline before Stage 9) but whose `origin.not_after` is past `clock_now`. The construction is a SHOULD-only violation of the operator-playbook alignment between `not_after` and `next_expected`; co-emission of `E_CANARY_EXPIRED` and `E_ORIGIN_EXPIRED` on the same vector is no longer reachable under rc.23 first-failing-stage precedence. +- **Warning-class diagnostics** (`W_CANARY_NEAR_EXPIRATION`, `W_CANARY_GAP`, `W_CANARY_UNAVAILABLE`, all `W_IMAGE_*`, `W_HISTORICAL_RENDERED`): require an `expected.warnings` extension to the vector schema, since warnings coexist with an `accept` verdict. `W_CANARY_EXPIRED` and `W_HISTORICAL_RUNTIME_AMBIGUOUS` were promoted to `E_CANARY_EXPIRED` and `E_HISTORICAL_RUNTIME_AMBIGUOUS` (both error) at rc.23 to align the catalog with the §08:183 and §10 (Historical content authorization) hard-block behavior; they are no longer in this group. +- **Image** (`W_IMAGE_*`, all 7 codes): require image bytes in `extra_files` and an `image_response.json` describing the fetched-content type/length; vector schema extension. +- **State** (`E_STATE_*`, all 7 codes): mostly publisher-side; require submit-flow vector schema. The Stage 5 manifest-validation portion of the submit-budget machinery (`E_SUBMIT_BUDGET` with `details.component = "state"`) IS covered by a single-document vector (143-submit-budget-state-overflow) and does not require the submit-flow extension; only the runtime client-side `E_STATE_TRANSMIT_BUDGET` and the other `E_STATE_*` codes that depend on transaction-document processing remain deferred. The Stage 5 vector counts each `value` at its raw `max_size` (UTF-8 byte length, no JSON-escape expansion), consistent with §07 `max_size` as a raw UTF-8 byte length. The escape-sensitive per-value wire boundary (a value whose JSON-escaped wire length exceeds its raw `max_size` and therefore overflows the §09 wire budget even when the Stage 5 envelope check passed) is a property of the deferred `E_STATE_TRANSMIT_BUDGET` runtime path; when the submit-flow tranche lands, that path is where the escaped-vs-raw boundary vectors belong. +- **Historical content** (`E_HISTORICAL_*` including `E_HISTORICAL_NO_PUBLICATION_PROOF`, `W_HISTORICAL_*`): require multi-manifest authorization-history scenarios. + +The following conditions are not vector-constructible within the wire-only scope of this corpus: + +- **`E_SIG_MALFORMED`**: per §11:173, this diagnostic only applies "in a context where stage-5 wire-side field-syntax validation does not apply". On the wire, signature length and base64url-alphabet violations are reported as `E_SCHEMA_FIELD_SYNTAX` at Stage 5 per §04 and §10 first-failing-stage precedence (exercised by vector 151). There is no wire-side construction that bypasses Stage 5 and reaches the Stage 6 raw-signature-decode path; the diagnostic is reachable only from out-of-band signature decoding (an implementation API surface that the corpus does not exercise). + +- **Freshness-unverified mode** (§10 "Clock reliability and the verified-time reference"): the trigger is a client-side property (no reliable current-time reference) that no document can induce. The corpus exercises a wire-to-verdict mapping in which each vector's input is a byte sequence and the verdict is the diagnostic the conforming validation pipeline produces; a condition whose trigger lives entirely outside that mapping cannot be a vector. Conforming clients exercise freshness-unverified mode through their clock-acquisition path, not through any document the corpus could supply. + +Vector-schema extensions (transport metadata, image responses, expected-warnings array, multi-manifest histories) are deferred to a future tranche. The current corpus exercises every diagnostic code reachable within the existing schema, except `E_SIG_MALFORMED` (not vector-constructible as documented above). diff --git a/src/test/resources/corpus/corpus.json b/src/test/resources/corpus/corpus.json new file mode 100644 index 0000000..4990101 --- /dev/null +++ b/src/test/resources/corpus/corpus.json @@ -0,0 +1,1079 @@ +{ + "_comment": "Generated by corpus/tools/generate.py. Do not hand-edit.", + "spec_version_target": "1.0", + "rc_target": "1.0-rc.27", + "keys": "keys.json", + "clock_now": "2026-05-07T00:01:00Z", + "vectors": [ + { + "id": "001-manifest-valid-minimal", + "kind": "manifest", + "description": "Minimal valid manifest signed by K_publisher. Empty state_policy and navigation. Tor v3 origin with derived address.", + "spec_refs": [ + "§02", + "§05", + "§06" + ], + "input": "vectors/001-manifest-valid-minimal/input.json", + "expected": { + "verdict": "accept" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "002-manifest-valid-state-policy", + "kind": "manifest", + "description": "Valid manifest declaring two state_policy entries: one request-mode session token, one client-only language preference.", + "spec_refs": [ + "§06", + "§07" + ], + "input": "vectors/002-manifest-valid-state-policy/input.json", + "expected": { + "verdict": "accept" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "003-content-valid-minimal", + "kind": "content", + "description": "Minimal valid content document with a single paragraph block. Signed by K_runtime authorized by manifest 001.", + "spec_refs": [ + "§02", + "§03", + "§05" + ], + "input": "vectors/003-content-valid-minimal/input.json", + "expected": { + "verdict": "accept" + }, + "context": { + "fetched_path": "/articles/first-post", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o" + } + }, + { + "id": "004-content-valid-blocks-showcase", + "kind": "content", + "description": "Valid content document exercising heading, marked paragraph, unordered list, code_block, divider, and quote. No image; image is exercised separately.", + "spec_refs": [ + "§03" + ], + "input": "vectors/004-content-valid-blocks-showcase/input.json", + "expected": { + "verdict": "accept" + }, + "context": { + "fetched_path": "/articles/blocks-showcase", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o" + } + }, + { + "id": "005-transaction-valid-minimal", + "kind": "transaction", + "description": "Minimal valid transaction document with a single feedback block. Carries a request_hash bound to the submit body in extra_files/submit_body.json.", + "spec_refs": [ + "§02", + "§09" + ], + "input": "vectors/005-transaction-valid-minimal/input.json", + "expected": { + "verdict": "accept" + }, + "context": { + "submit_path": "/contact", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o", + "submit_body_path": "vectors/005-transaction-valid-minimal/submit_body.json" + }, + "extra_files": [ + "submit_body.json" + ] + }, + { + "id": "006-manifest-valid-not-after", + "kind": "manifest", + "description": "Valid manifest declaring origin.not_after = 2027-05-07T00:00:00Z, one year after canary.issued_at and well within the 5-year ceiling. At clock_now (2026-05-07) the manifest is not yet origin-expired. Exercises the rc.14 origin-not-after schema acceptance and Stage 5 cross-field semantic checks (strictly later than canary.issued_at; not more than 5 years after; SHOULD later than canary.next_expected - satisfied here).", + "spec_refs": [ + "§06", + "§10" + ], + "input": "vectors/006-manifest-valid-not-after/input.json", + "expected": { + "verdict": "accept" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "007-content-valid-large-seq", + "kind": "content", + "description": "Valid content document whose seq = 9007199254740993 (2^53 + 1), the smallest integer with no IEEE 754 binary64 representation. Per §04 integer serialization, Entangled integers up to 2^63-1 are canonicalized as exact decimal across the whole range, overriding the binary64 interpretation above 2^53. The K_runtime signature is computed over the exact-decimal canonical form. A conforming verifier MUST accept; an implementation that routes the seq through a binary64 double serializes 9007199254740992 and fails signature verification. Signed by K_runtime authorized by manifest 001.", + "spec_refs": [ + "§02", + "§04", + "§05" + ], + "input": "vectors/007-content-valid-large-seq/input.json", + "expected": { + "verdict": "accept" + }, + "context": { + "fetched_path": "/articles/large-seq", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o" + } + }, + { + "id": "100-input-bom", + "kind": "manifest", + "description": "Otherwise-valid manifest preceded by a UTF-8 BOM (EF BB BF). Must be rejected at stage 2 input checks.", + "spec_refs": [ + "§04" + ], + "input": "vectors/100-input-bom/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_INPUT_BOM" + } + }, + { + "id": "101-input-bad-utf8", + "kind": "manifest", + "description": "Body is not valid UTF-8: contains a lone 0xFE byte that is not part of any UTF-8 sequence. Must be rejected at stage 2.", + "spec_refs": [ + "§04" + ], + "input": "vectors/101-input-bad-utf8/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_INPUT_UTF8" + } + }, + { + "id": "110-parse-duplicate-keys", + "kind": "content", + "description": "Content document with a duplicate top-level member name (\"path\" appears twice). Must be rejected at stage 3 with E_PARSE_DUPLICATE_KEY before schema validation.", + "spec_refs": [ + "§04" + ], + "input": "vectors/110-parse-duplicate-keys/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_PARSE_DUPLICATE_KEY" + } + }, + { + "id": "120-spec-version-wrong", + "kind": "manifest", + "description": "Document declaring spec_version \"1.1\". A v1.0 client must reject with E_KIND_SPEC_VERSION before schema validation.", + "spec_refs": [ + "§02", + "§11" + ], + "input": "vectors/120-spec-version-wrong/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_KIND_SPEC_VERSION" + } + }, + { + "id": "121-kind-unknown", + "kind": "manifest", + "description": "Document whose kind is \"unknown\" (not one of manifest/content/transaction). Rejected at stage 4 with E_KIND_UNKNOWN.", + "spec_refs": [ + "§02", + "§11" + ], + "input": "vectors/121-kind-unknown/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_KIND_UNKNOWN" + } + }, + { + "id": "130-schema-unknown-field", + "kind": "manifest", + "description": "Manifest with an extra top-level field \"unexpected_field\". Closed-schema discipline rejects at stage 5 with E_SCHEMA_UNKNOWN_FIELD before signature verification.", + "spec_refs": [ + "§02", + "§06" + ], + "input": "vectors/130-schema-unknown-field/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_UNKNOWN_FIELD" + } + }, + { + "id": "131-schema-missing-required", + "kind": "manifest", + "description": "Manifest with required field min_refresh_interval omitted. Rejected at stage 5 with E_SCHEMA_REQUIRED_FIELD.", + "spec_refs": [ + "§06" + ], + "input": "vectors/131-schema-missing-required/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_REQUIRED_FIELD" + } + }, + { + "id": "132-schema-null-value", + "kind": "manifest", + "description": "Manifest where navigation is null. All other required fields are present and well-formed; only the null literal triggers stage 5 rejection. E_SCHEMA_NULL_VALUE.", + "spec_refs": [ + "§04", + "§06" + ], + "input": "vectors/132-schema-null-value/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_NULL_VALUE" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "133-schema-block-kind-unknown", + "kind": "content", + "description": "Content document with a block whose kind is \"marquee\", a syntactically valid slug not in the enumerated block kinds (§03). Stage 5 schema rejection. E_SCHEMA_ENUM_VIOLATION.", + "spec_refs": [ + "§03", + "§11" + ], + "input": "vectors/133-schema-block-kind-unknown/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_ENUM_VIOLATION" + }, + "context": { + "fetched_path": "/articles/bad-block", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o" + } + }, + { + "id": "140-numeric-float", + "kind": "manifest", + "description": "Manifest where min_refresh_interval has a float-shape token (3600.0). The strict integer grammar rejects floats lexically. E_SCHEMA_NON_INTEGER.", + "spec_refs": [ + "§04" + ], + "input": "vectors/140-numeric-float/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_NON_INTEGER" + } + }, + { + "id": "141-numeric-exponent", + "kind": "manifest", + "description": "Manifest where min_refresh_interval is written in exponent form (3.6e3). Integer grammar rejects exponents. E_SCHEMA_NON_INTEGER.", + "spec_refs": [ + "§04" + ], + "input": "vectors/141-numeric-exponent/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_NON_INTEGER" + } + }, + { + "id": "142-numeric-overflow", + "kind": "manifest", + "description": "Manifest where min_refresh_interval is 9223372036854775808 (= 2^63), one above the protocol's 64-bit signed integer cap. All other required fields are present and well-formed. E_SCHEMA_NON_INTEGER.", + "spec_refs": [ + "§04", + "§06" + ], + "input": "vectors/142-numeric-overflow/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_NON_INTEGER" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "143-submit-budget-state-overflow", + "kind": "manifest", + "description": "Manifest whose state_policy declares 32 request-mode entries each with max_size = 2048 bytes. Aggregate worst-case encoded wire contribution to request_state is 66879 bytes (32 entries * (2048 raw value bytes + 41 envelope bytes: 36 fixed + 2 namespace + 3 key) + 31 commas), exceeding the state_budget of 53248 bytes defined in §09 ('Submit body budget partition') by 13631 bytes. The value is counted at its raw max_size (UTF-8 byte length, no JSON-escape expansion) per §07 max_size. Per §07 'Submit budget satisfiability', rejected at Stage 5 schema validation as E_SUBMIT_BUDGET with details.component = 'state'. Manifest is signed correctly; the satisfiability violation is the only live Stage 5 violation.", + "spec_refs": [ + "§07", + "§09", + "§11" + ], + "input": "vectors/143-submit-budget-state-overflow/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SUBMIT_BUDGET", + "diagnostic_details": { + "component": "state", + "declared_bytes": 66879, + "budget_bytes": 53248 + } + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "150-sig-modified-payload", + "kind": "manifest", + "description": "Otherwise-valid manifest whose min_refresh_interval was changed after signing. The wire signature no longer verifies. E_SIG_VERIFICATION.", + "spec_refs": [ + "§05" + ], + "input": "vectors/150-sig-modified-payload/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SIG_VERIFICATION" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "151-sig-syntax-length", + "kind": "manifest", + "description": "Manifest whose sig field is 43 ASCII characters instead of the canonical 86. §04 declared-length check at stage 5 rejects with E_SCHEMA_FIELD_SYNTAX before stage 6 signature decoding fires (§10 first-failing-stage precedence).", + "spec_refs": [ + "§04", + "§02" + ], + "input": "vectors/151-sig-syntax-length/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_SYNTAX" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "152-sig-non-canonical-s", + "kind": "manifest", + "description": "Manifest with a signature whose S component is non-canonical (S' = S + L >= L). The signature would verify under cofactored Ed25519, but the strict profile (§05) rejects non-canonical S. E_SIG_VERIFICATION.", + "spec_refs": [ + "§05" + ], + "input": "vectors/152-sig-non-canonical-s/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SIG_VERIFICATION" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "153-sig-small-order-pubkey", + "kind": "manifest", + "description": "Manifest where publisher_pubkey is the encoded identity point (small-order, order 1). The strict profile (§05) rejects small-order public keys before signature verification; E_SIG_VERIFICATION.", + "spec_refs": [ + "§05" + ], + "input": "vectors/153-sig-small-order-pubkey/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SIG_VERIFICATION" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "160-base64url-padded", + "kind": "manifest", + "description": "Manifest whose sig field carries '=' padding. Strict base64url decoding rejects with E_SCHEMA_FIELD_SYNTAX before signature verification.", + "spec_refs": [ + "§04", + "§02" + ], + "input": "vectors/160-base64url-padded/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_SYNTAX" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "161-base64url-standard-alphabet", + "kind": "manifest", + "description": "Manifest whose sig field uses the standard base64 alphabet (+ and /) instead of the URL-safe alphabet (- and _). Rejected with E_SCHEMA_FIELD_SYNTAX.", + "spec_refs": [ + "§04" + ], + "input": "vectors/161-base64url-standard-alphabet/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_SYNTAX" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "162-base64url-whitespace", + "kind": "manifest", + "description": "Manifest whose sig field contains an embedded space character. Strict base64url rejects whitespace; E_SCHEMA_FIELD_SYNTAX.", + "spec_refs": [ + "§04" + ], + "input": "vectors/162-base64url-whitespace/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_SYNTAX" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "170-bind-path-mismatch", + "kind": "content", + "description": "Otherwise-valid content document whose path field is /articles/foo, fetched from /articles/bar. Stage 9 path binding rejects with E_BIND_PATH.", + "spec_refs": [ + "§02", + "§10" + ], + "input": "vectors/170-bind-path-mismatch/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_BIND_PATH" + }, + "context": { + "fetched_path": "/articles/bar", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o" + } + }, + { + "id": "171-bind-reserved-manifest-path", + "kind": "content", + "description": "Content document declaring path /manifest.json. The path is reserved for manifest fetches and the schema rejects it with E_SCHEMA_FIELD_SYNTAX.", + "spec_refs": [ + "§02", + "§09" + ], + "input": "vectors/171-bind-reserved-manifest-path/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_SYNTAX" + }, + "context": { + "fetched_path": "/manifest.json", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o" + } + }, + { + "id": "172-bind-request-hash-mismatch", + "kind": "transaction", + "description": "Transaction document whose request_hash matches the original submit body, but the client's recorded submit body has been tampered (fields.message changed). Stage 9 rejects with E_BIND_REQUEST_HASH.", + "spec_refs": [ + "§02", + "§09" + ], + "input": "vectors/172-bind-request-hash-mismatch/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_BIND_REQUEST_HASH" + }, + "context": { + "submit_path": "/contact", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o", + "submit_body_path": "vectors/172-bind-request-hash-mismatch/submit_body.json" + }, + "extra_files": [ + "submit_body.json" + ] + }, + { + "id": "180-canary-equal-issued-at-conflict", + "kind": "manifest", + "description": "Two manifests with the same canary.issued_at and the same K_publisher.pub but different runtime_pubkey. Once 001 is verified and retained, observing this manifest at the same issued_at must produce E_CANARY_CONFLICT.", + "spec_refs": [ + "§08" + ], + "input": "vectors/180-canary-equal-issued-at-conflict/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_CANARY_CONFLICT" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion", + "previously_verified": "vectors/001-manifest-valid-minimal/input.json" + } + }, + { + "id": "154-sig-non-canonical-r", + "kind": "manifest", + "description": "Manifest whose signature R has a non-canonical compressed point encoding (y >= p). The strict profile (§05) rejects non-canonical encodings of R independently of any verification equation. E_SIG_VERIFICATION.", + "spec_refs": [ + "§05" + ], + "input": "vectors/154-sig-non-canonical-r/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SIG_VERIFICATION" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "155-sig-non-canonical-a", + "kind": "manifest", + "description": "Manifest whose publisher_pubkey is a non-canonical Ed25519 point encoding (y >= p). The strict profile (§05) rejects non-canonical encodings of A before signature verification. E_SIG_VERIFICATION.", + "spec_refs": [ + "§05" + ], + "input": "vectors/155-sig-non-canonical-a/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SIG_VERIFICATION" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "181-canary-issued-at-downgrade", + "kind": "manifest", + "description": "Manifest with canary.issued_at strictly older than the issued_at of a previously verified manifest (001) for the same K_publisher.pub. The client's anti-downgrade rule (§08) rejects with E_CANARY_DOWNGRADE.", + "spec_refs": [ + "§08" + ], + "input": "vectors/181-canary-issued-at-downgrade/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_CANARY_DOWNGRADE" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion", + "previously_verified": "vectors/001-manifest-valid-minimal/input.json" + } + }, + { + "id": "190-unicode-nfd-statement", + "kind": "manifest", + "description": "Manifest whose canary.statement contains a decomposed combining mark (NFD) rather than the precomposed NFC form. Per §04, user-visible strings must be in NFC. Rejected at schema validation with E_SCHEMA_FIELD_SYNTAX before signature verification.", + "spec_refs": [ + "§04", + "§08" + ], + "input": "vectors/190-unicode-nfd-statement/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_SYNTAX" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "200-migration-successor-origin-expired", + "kind": "manifest", + "description": "Migration scenario exercising the rc.15 successor_stage9_failure path. The announcing manifest at the original origin is itself valid and accepted in isolation; it carries a migration_pointer to a successor origin. The successor manifest (in extra_files/successor_manifest.json) is signed correctly by the same K_publisher and binds to the successor address per the Tor v3 derivation rule, but its own origin.not_after has already passed at clock_now (2026-05-07). The successor fails Stage 9 in isolation with E_ORIGIN_EXPIRED. Per §10 'Successor verification', the migration is rejected under E_MIGRATION_MISMATCH; per §11, details.mismatch_field = 'successor_stage9_failure' and details.underlying_diagnostic_code = 'E_ORIGIN_EXPIRED'. The reject verdict here refers to the migration adoption outcome, not to the announcing manifest itself.", + "spec_refs": [ + "§06", + "§10", + "§11" + ], + "input": "vectors/200-migration-successor-origin-expired/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_MIGRATION_MISMATCH", + "diagnostic_details": { + "mismatch_field": "successor_stage9_failure", + "underlying_diagnostic_code": "E_ORIGIN_EXPIRED" + } + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion", + "successor_origin_address": "fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion", + "successor_manifest_path": "vectors/200-migration-successor-origin-expired/successor_manifest.json" + }, + "extra_files": [ + "successor_manifest.json" + ] + }, + { + "id": "102-input-byte-cap", + "kind": "manifest", + "description": "Manifest-shaped body padded past the 64 KiB byte cap for manifests (§02). Stage 2 input check fires before Stage 3 JSON parsing; well-formedness of the JSON below the cap is therefore irrelevant. E_INPUT_BYTE_CAP.", + "spec_refs": [ + "§02", + "§04" + ], + "input": "vectors/102-input-byte-cap/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_INPUT_BYTE_CAP" + } + }, + { + "id": "111-parse-nesting-depth", + "kind": "manifest", + "description": "Manifest body containing a 20-level-deep nested array, exceeding the 16-level Stage 3 nesting cap (§04). The field below the cap is irrelevant; Stage 3 fires before Stage 5. E_PARSE_NESTING_DEPTH.", + "spec_refs": [ + "§04" + ], + "input": "vectors/111-parse-nesting-depth/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_PARSE_NESTING_DEPTH" + } + }, + { + "id": "112-parse-string-length", + "kind": "content", + "description": "Content document containing a single string of 102401 ASCII bytes, one byte above the 100 KiB Stage 3 string cap (§04). Body remains under the 1 MiB content byte cap so Stage 2 passes and Stage 3 fires. E_PARSE_STRING_LENGTH.", + "spec_refs": [ + "§04" + ], + "input": "vectors/112-parse-string-length/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_PARSE_STRING_LENGTH" + } + }, + { + "id": "113-parse-array-length", + "kind": "content", + "description": "Content document whose blocks array contains 10001 entries, one above the 10000-element Stage 3 array cap (§04). Element shapes are irrelevant; Stage 3 fires before Stage 5. E_PARSE_ARRAY_LENGTH.", + "spec_refs": [ + "§04" + ], + "input": "vectors/113-parse-array-length/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_PARSE_ARRAY_LENGTH" + } + }, + { + "id": "114-parse-object-keys", + "kind": "content", + "description": "Content document whose meta object contains 257 members, one above the 256-key Stage 3 object cap (§04). Member names beyond the meta schema are irrelevant; Stage 3 fires before Stage 5. E_PARSE_OBJECT_KEYS.", + "spec_refs": [ + "§04" + ], + "input": "vectors/114-parse-object-keys/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_PARSE_OBJECT_KEYS" + } + }, + { + "id": "115-parse-json-malformed", + "kind": "manifest", + "description": "Body is not parseable as JSON: trailing comma followed by a missing value at \"sig\". Stage 3 JSON parsing rejects with E_PARSE_JSON.", + "spec_refs": [ + "§04" + ], + "input": "vectors/115-parse-json-malformed/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_PARSE_JSON" + } + }, + { + "id": "122-kind-missing-fields", + "kind": "manifest", + "description": "Document with the top-level sig field omitted. spec_version and kind are present and well-formed, but sig - one of the three top-level required fields per §02 - is absent. Stage 4 kind discrimination fires E_KIND_MISSING_FIELDS.", + "spec_refs": [ + "§02", + "§11" + ], + "input": "vectors/122-kind-missing-fields/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_KIND_MISSING_FIELDS" + } + }, + { + "id": "134-schema-field-type", + "kind": "manifest", + "description": "Manifest where min_refresh_interval is the string \"3600\" instead of a non-negative integer. Stage 5 closed-schema validation fires before Stage 6 signature verification (§10 first-failing-stage). E_SCHEMA_FIELD_TYPE.", + "spec_refs": [ + "§06", + "§11" + ], + "input": "vectors/134-schema-field-type/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_TYPE" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "135-schema-field-range", + "kind": "content", + "description": "Content document containing a heading block whose level is 7, outside the [1..6] range required by §03. Stage 5 schema rejects with E_SCHEMA_FIELD_RANGE before Stage 6 signature verification.", + "spec_refs": [ + "§03", + "§11" + ], + "input": "vectors/135-schema-field-range/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_RANGE" + }, + "context": { + "fetched_path": "/articles/bad-range", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o" + } + }, + { + "id": "136-schema-block-not-permitted", + "kind": "transaction", + "description": "Transaction document whose blocks array contains a submit_form block. submit_form is permitted only in content documents per §03 \"Block usage by document kind\". Stage 5 schema rejects with E_SCHEMA_BLOCK_NOT_PERMITTED.", + "spec_refs": [ + "§02", + "§03" + ], + "input": "vectors/136-schema-block-not-permitted/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_BLOCK_NOT_PERMITTED" + }, + "context": { + "submit_path": "/contact", + "expected_runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o", + "submit_body_path": "vectors/136-schema-block-not-permitted/submit_body.json" + }, + "extra_files": [ + "submit_body.json" + ] + }, + { + "id": "137-schema-duplicate-entry", + "kind": "manifest", + "description": "Manifest whose state_policy contains two entries with identical (namespace, key) = (\"session\", \"auth\"). §06 requires (namespace, key) uniqueness across state_policy entries. Stage 5 schema rejects with E_SCHEMA_DUPLICATE_ENTRY.", + "spec_refs": [ + "§06", + "§07", + "§11" + ], + "input": "vectors/137-schema-duplicate-entry/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_DUPLICATE_ENTRY" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "138-schema-malformed-unicode", + "kind": "manifest", + "description": "Manifest whose canary.statement contains the JSON escape sequence \\uD800 - a lone high surrogate with no paired low surrogate. After JSON parsing this yields a string with an isolated surrogate code point, which §04 rejects as malformed Unicode at Stage 5 schema validation (before Stage 6 signature verification). Conforming parsers accept the JSON escape per RFC 8259 §7; rejection is at the schema layer. E_SCHEMA_MALFORMED_UNICODE.", + "spec_refs": [ + "§04", + "§11" + ], + "input": "vectors/138-schema-malformed-unicode/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_MALFORMED_UNICODE" + } + }, + { + "id": "175-bind-origin", + "kind": "manifest", + "description": "Otherwise-valid manifest whose origin binds to K_origin (test fixture), fetched from the onion address derived from K_origin_2. Stage 9 Tor v3 address-to-key derivation produces an origin_pubkey that does not match manifest.origin.origin_pubkey. E_BIND_ORIGIN.", + "spec_refs": [ + "§05", + "§09", + "§10" + ], + "input": "vectors/175-bind-origin/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_BIND_ORIGIN" + }, + "context": { + "fetched_origin_address": "fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion" + } + }, + { + "id": "176-origin-invalid", + "kind": "manifest", + "description": "Manifest whose origin.not_after equals canary.issued_at (2026-05-07T00:00:00Z). §06 requires not_after strictly later than canary.issued_at; equality violates the MUST. The manifest is signed correctly and otherwise valid. E_ORIGIN_INVALID.", + "spec_refs": [ + "§06", + "§11" + ], + "input": "vectors/176-origin-invalid/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_ORIGIN_INVALID" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "182-canary-invalid", + "kind": "manifest", + "description": "Manifest whose canary interval (next_expected - issued_at) is 6 days, below the 7-day minimum required by §08. The manifest is otherwise well-formed and signed correctly; canary is fresh at clock_now (2026-05-07). Stage 8 canary validation fires E_CANARY_INVALID.", + "spec_refs": [ + "§08", + "§11" + ], + "input": "vectors/182-canary-invalid/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_CANARY_INVALID" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "139-schema-field-length", + "kind": "manifest", + "description": "Manifest whose canary.freshness_proof is a 201-byte ASCII string, one byte above the 200-byte cap declared in §08:118. The string is well within the Stage 3 100 KiB parser cap (§04) so Stage 3 passes; Stage 5 schema validation fires E_SCHEMA_FIELD_LENGTH for the field-specific cap, distinct from the parser-level E_PARSE_STRING_LENGTH (vector 112).", + "spec_refs": [ + "§08", + "§11" + ], + "input": "vectors/139-schema-field-length/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_LENGTH" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "156-sig-invalid-key-no-manifest", + "kind": "content", + "description": "Content document presented without any verified manifest supplying an authorized runtime_pubkey for the publisher. Per §11:172,175 the absence of the expected verification key yields E_SIG_INVALID_KEY, distinct from E_SIG_VERIFICATION which requires a key that decodes and fails the verify equation. The content body and signature are themselves well-formed; the failure is the missing key context. Vector context deliberately omits expected_runtime_pubkey and previously_verified.", + "spec_refs": [ + "§05", + "§11" + ], + "input": "vectors/156-sig-invalid-key-no-manifest/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SIG_INVALID_KEY" + }, + "context": { + "fetched_path": "/articles/orphan-content" + } + }, + { + "id": "157-sig-small-order-r", + "kind": "manifest", + "description": "Manifest whose signature R component is the encoded identity point (small-order, order 1), the same encoding pattern as vector 153 uses for the public key A. Per §05:174 (rc.22 N63), the strict profile rejects small-order R alongside small-order A, matching the verify_strict mode in ed25519-dalek (src/verifying.rs) which calls signature_R.is_small_order() before the verify equation. Pair to vector 153 for A; both fire E_SIG_VERIFICATION.", + "spec_refs": [ + "§05", + "§11" + ], + "input": "vectors/157-sig-small-order-r/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SIG_VERIFICATION" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "177-origin-invalid-beyond-5y", + "kind": "manifest", + "description": "Manifest whose origin.not_after is 2031-05-08, more than 5 years after canary.issued_at (2026-05-07). §06 forbids not_after beyond 5 years past issued_at. This vector pairs with 176 (equal-to-issued_at reason) to cover both reason values of E_ORIGIN_INVALID structured details (not_after_beyond_5y vs not_after_not_later_than_issued_at).", + "spec_refs": [ + "§06", + "§11" + ], + "input": "vectors/177-origin-invalid-beyond-5y/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_ORIGIN_INVALID", + "diagnostic_details": { + "reason": "not_after_beyond_5y" + } + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "178-manifest-updated-future-skew", + "kind": "manifest", + "description": "Manifest whose `updated` is set 6 minutes ahead of clock_now (2026-05-07T00:07:00Z vs 2026-05-07T00:01:00Z), exceeding the 300-second future-skew tolerance defined in §10. Per §06:342 and §10:815, this is rejected as E_SCHEMA_FIELD_SYNTAX with structured details reason=future_beyond_skew_tolerance. The manifest is signed correctly and otherwise valid; the temporal-domain failure is the only live violation at Stage 5.", + "spec_refs": [ + "§06", + "§10", + "§11" + ], + "input": "vectors/178-manifest-updated-future-skew/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_SYNTAX", + "diagnostic_details": { + "reason": "future_beyond_skew_tolerance" + } + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "183-canary-issued-at-future-skew", + "kind": "manifest", + "description": "Manifest whose canary.issued_at is 2026-05-07T00:07:00Z, 6 minutes ahead of clock_now (2026-05-07T00:01:00Z), exceeding the 300-second future-skew tolerance defined in §10. Per §08:68,156 this is one of the named E_CANARY_INVALID conditions. The manifest signature is valid, the canary interval is within bounds, and `updated` is kept at clock_now-1 to avoid competing with the §06 future-skew check exercised by vector 178: the Stage 8 issued_at check is the only live skew violation.", + "spec_refs": [ + "§08", + "§10", + "§11" + ], + "input": "vectors/183-canary-issued-at-future-skew/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_CANARY_INVALID" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "184-canary-runtime-reuse", + "kind": "manifest", + "description": "Multi-manifest scenario: a previously verified manifest dated 2026-04-30 (carried in extra_files as prior_manifest.json) authorizes runtime_pubkey X. The presented manifest at clock_now (issued_at 2026-05-07) for the same K_publisher.pub declares the same runtime_pubkey X. Per §08 (rc.19 N55) and §11:200, rotation MUST produce a distinct runtime_pubkey; reuse is rejected as E_CANARY_RUNTIME_REUSE at Stage 8. Both manifests are signed correctly and otherwise valid; the rotation-proof failure is the only live Stage 8 violation.", + "spec_refs": [ + "§08", + "§11" + ], + "input": "vectors/184-canary-runtime-reuse/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_CANARY_RUNTIME_REUSE", + "diagnostic_details": { + "runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o", + "previous_issued_at": "2026-04-30T00:00:00Z", + "current_issued_at": "2026-05-07T00:00:00Z", + "window_position": 1 + } + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion", + "previously_verified": "vectors/184-canary-runtime-reuse/prior_manifest.json" + }, + "extra_files": [ + "prior_manifest.json" + ] + }, + { + "id": "185-canary-runtime-reuse-resurrection", + "kind": "manifest", + "description": "A -> B -> A resurrection scenario. The publisher history (extra_files) contains M_A (issued_at 2026-04-23, runtime_pubkey X) and M_B (issued_at 2026-04-30, runtime_pubkey Y). The presented manifest M_C at clock_now (issued_at 2026-05-07) declares runtime_pubkey X again, resurrecting the M_A key after M_B retired it. Per §08 immediate-preceding MUST, M_C passes (X != Y). Per §08 SHOULD for clients maintaining runtime-pubkey history, M_C is rejected as E_CANARY_RUNTIME_REUSE with details.window_position = 2 (M_A is two entries back). Stateless clients accept (per §00 N60 limitation); the corpus verdict records the stateful-client rejection.", + "spec_refs": [ + "§08", + "§00", + "§11" + ], + "input": "vectors/185-canary-runtime-reuse-resurrection/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_CANARY_RUNTIME_REUSE", + "diagnostic_details": { + "runtime_pubkey": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o", + "previous_issued_at": "2026-04-23T00:00:00Z", + "current_issued_at": "2026-05-07T00:00:00Z", + "window_position": 2 + } + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion", + "previously_verified_history": [ + "vectors/185-canary-runtime-reuse-resurrection/prior_manifest_a.json", + "vectors/185-canary-runtime-reuse-resurrection/prior_manifest_b.json" + ] + }, + "extra_files": [ + "prior_manifest_a.json", + "prior_manifest_b.json" + ] + }, + { + "id": "191-unicode-nfd-freshness-proof", + "kind": "manifest", + "description": "Manifest whose canary.freshness_proof contains a decomposed combining mark (NFD) rather than the precomposed NFC form. Per §04 and the §08 explicit NFC rule for freshness_proof (rc.19 N59), user-visible strings must be in NFC. Rejected at schema validation with E_SCHEMA_FIELD_SYNTAX before signature verification. Parity with vector 190 for canary.statement.", + "spec_refs": [ + "§04", + "§08" + ], + "input": "vectors/191-unicode-nfd-freshness-proof/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_SCHEMA_FIELD_SYNTAX" + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + { + "id": "201-migration-chain-cycle", + "kind": "manifest", + "description": "Two-manifest chain-cycle scenario A -> B -> A. The announcing manifest at origin A carries a migration_pointer to successor B (op_pub_2). The successor at B is signed correctly and binds correctly, but its own migration_pointer announces a return to A. Per §10:436, the per-flow visited_origins set forbids re-adopting an address already visited; B's announcement of A is rejected as E_MIGRATION_INVALID with details.reason='chain_cycle'. The diagnostic is deterministic across conforming clients (any client tracking visited_origins per §10 rejects on the second hop, regardless of chain-depth policy).", + "spec_refs": [ + "§06", + "§10", + "§11" + ], + "input": "vectors/201-migration-chain-cycle/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_MIGRATION_INVALID", + "diagnostic_details": { + "reason": "chain_cycle", + "announcing_origin_address": "fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion", + "successor_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion", + "successor_origin_address": "fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion", + "successor_manifest_path": "vectors/201-migration-chain-cycle/successor_manifest.json" + }, + "extra_files": [ + "successor_manifest.json" + ] + }, + { + "id": "202-migration-successor-key-mismatch", + "kind": "manifest", + "description": "Announcement-internal successor binding failure (§06). The migration_pointer.successor_origin.address decodes to op_pub_2 but the declared successor_origin.origin_pubkey is op_pub, so the address does not decode to the declared key. Per §06 the client MUST verify this binding before treating the announcement as valid; failure is E_MIGRATION_INVALID with details.reason='successor_key_mismatch'. The check is evaluated on the announcing manifest alone and does not fetch the successor (distinct from the §10 fetch-time E_MIGRATION_MISMATCH path).", + "spec_refs": [ + "§05", + "§06", + "§11" + ], + "input": "vectors/202-migration-successor-key-mismatch/input.json", + "expected": { + "verdict": "reject", + "diagnostic": "E_MIGRATION_INVALID", + "diagnostic_details": { + "reason": "successor_key_mismatch", + "announcing_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion", + "successor_origin_address": "fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion" + } + }, + "context": { + "fetched_origin_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + } + } + ] +} diff --git a/src/test/resources/corpus/keys.json b/src/test/resources/corpus/keys.json new file mode 100644 index 0000000..ee8badd --- /dev/null +++ b/src/test/resources/corpus/keys.json @@ -0,0 +1,26 @@ +{ + "_comment": "Test fixtures only. NEVER use these for any real deployment.", + "publisher": { + "seed_hex": "454e54414e474c45442d76312e302d7075626c69736865722d74657374303100", + "pub_b64u": "moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc", + "pip": "once grain trumpet rookie common appear canyon blur eye guide small betray tissue depth mutual swift admit text level practice hunt accuse hobby unusual" + }, + "runtime": { + "seed_hex": "454e54414e474c45442d76312e302d72756e74696d652d746573743030303100", + "pub_b64u": "jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o" + }, + "origin": { + "seed_hex": "454e54414e474c45442d76312e302d6f726967696e2d74657374303030303100", + "pub_b64u": "Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo", + "tor_v3_address": "dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion" + }, + "runtime_2": { + "seed_hex": "454e54414e474c45442d76312e302d72756e74696d652d746573743030303200", + "pub_b64u": "rN5KJ_PD7wcccfXJTgJVZIRG5bd8O-ZYhgdImGhD980" + }, + "origin_2": { + "seed_hex": "454e54414e474c45442d76312e302d6f726967696e2d74657374303030303200", + "pub_b64u": "Ko4-lXCoSgOUc_2Dt2Bi9NcyVyUN3GYgEKMJkgPQtb8", + "tor_v3_address": "fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion" + } +} diff --git a/src/test/resources/corpus/tools/bip39_english.txt b/src/test/resources/corpus/tools/bip39_english.txt new file mode 100644 index 0000000..942040e --- /dev/null +++ b/src/test/resources/corpus/tools/bip39_english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo diff --git a/src/test/resources/corpus/tools/generate.py b/src/test/resources/corpus/tools/generate.py new file mode 100644 index 0000000..56c4179 --- /dev/null +++ b/src/test/resources/corpus/tools/generate.py @@ -0,0 +1,2301 @@ +#!/usr/bin/env python3 +""" +Entangled v1.0 conformance corpus generator. + +Produces a deterministic corpus of test vectors for Entangled v1.0 protocol +implementations. Each vector is a complete signed (or deliberately broken) +document with a documented expected verdict (accept / reject + diagnostic). + +Run from the repository root: + + python3 corpus/tools/generate.py + +Outputs are written to corpus/keys.json, corpus/corpus.json, and +corpus/vectors//. + +Determinism: Ed25519 keys are derived from fixed 32-byte seeds. Signing under +RFC 8032 is deterministic. The output is reproducible byte-for-byte. + +Requirements: Python 3.10+, cryptography>=3.4 (for raw Ed25519). +""" +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import shutil +import sys +from pathlib import Path + +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, +) +from cryptography.hazmat.primitives import serialization + +# Repository root, relative to this script (corpus/tools/generate.py). +ROOT = Path(__file__).resolve().parent.parent +VECTORS_DIR = ROOT / "vectors" + +# BIP-39 English wordlist, bundled at corpus/tools/bip39_english.txt. Used to +# compute the publisher's PIP for the derivation vector. The file is the +# canonical wordlist from the Bitcoin BIPs repository +# (bitcoin/bips: bip-0039/english.txt). Its SHA-256 is recorded in +# corpus/README.md for verification. +BIP39_WORDLIST_PATH = Path(__file__).resolve().parent / "bip39_english.txt" + +# --------------------------------------------------------------------------- +# Test key seeds. Fixed for reproducibility. NEVER use these for any real +# deployment; they are public test fixtures. +# --------------------------------------------------------------------------- +PUBLISHER_SEED = b"ENTANGLED-v1.0-publisher-test01\x00" +RUNTIME_SEED = b"ENTANGLED-v1.0-runtime-test0001\x00" +ORIGIN_SEED = b"ENTANGLED-v1.0-origin-test00001\x00" +RUNTIME_SEED_2 = b"ENTANGLED-v1.0-runtime-test0002\x00" +ORIGIN_SEED_2 = b"ENTANGLED-v1.0-origin-test00002\x00" + +assert len(PUBLISHER_SEED) == 32 +assert len(RUNTIME_SEED) == 32 +assert len(ORIGIN_SEED) == 32 +assert len(RUNTIME_SEED_2) == 32 +assert len(ORIGIN_SEED_2) == 32 + + +# --------------------------------------------------------------------------- +# Crypto helpers +# --------------------------------------------------------------------------- +def b64u(data: bytes) -> str: + """RFC 4648 §5 base64url, no padding.""" + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +def b64u_decode(s: str) -> bytes: + """Decode base64url with strict padding rules; only used in tooling.""" + pad = "=" * (-len(s) % 4) + return base64.urlsafe_b64decode(s + pad) + + +# Order of the Ed25519 base point (RFC 8032). Used to construct non-canonical +# S signatures (S' = S + L) for the strict-profile S-canonicalization test. +ED25519_L = 2**252 + 27742317777372353535851937790883648493 + +# Prime defining the Ed25519 base field: p = 2^255 - 19 (RFC 8032). +ED25519_P = 2**255 - 19 + + +def non_canonical_s(sig_bytes: bytes) -> bytes: + """Given a valid 64-byte Ed25519 signature R||S, return R||(S + L). + + Under cofactored verification, the resulting signature still verifies + because [L]B is the identity. Under the strict profile, S + L >= L is + non-canonical and rejected. The vector exercises strict-profile S-range + enforcement. + """ + assert len(sig_bytes) == 64 + R = sig_bytes[:32] + S = int.from_bytes(sig_bytes[32:], "little") + S_prime = S + ED25519_L + return R + S_prime.to_bytes(32, "little") + + +def non_canonical_r_encoding() -> bytes: + """Return a 32-byte non-canonical Ed25519 point encoding. + + The y-coordinate portion (low 255 bits) is 2^255 - 1, which exceeds the + field prime p = 2^255 - 19. RFC 8032 requires y < p; the strict profile + rejects encodings with y >= p. The x-sign bit (top bit of byte 31) is + arbitrary; using 1 here keeps the encoding all-0xff for clarity. + """ + return bytes([0xFF] * 32) + + +# Small-order Ed25519 public key: the identity point. Compressed encoding is +# the little-endian byte representation of the y-coordinate, with the high bit +# of the last byte carrying the x-sign. For the identity, y = 1 and x = 0, so +# the encoding is 0x01 followed by 31 zero bytes. +SMALL_ORDER_A = bytes([0x01]) + bytes(31) + + +def keypair(seed: bytes) -> tuple[Ed25519PrivateKey, bytes]: + """Derive an Ed25519 keypair from a 32-byte seed. Returns (priv, pub_bytes).""" + priv = Ed25519PrivateKey.from_private_bytes(seed) + pub = priv.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + return priv, pub + + +def jcs(obj) -> bytes: + """JCS canonicalization (RFC 8785) for the Entangled JSON subset. + + Entangled uses integer-only numbers, no nulls, no duplicate keys, and + valid UTF-8 strings, so JCS reduces to: sort object keys lex, no + whitespace, JSON-standard escaping, UTF-8 output. + + NOTE: This helper is sufficient ONLY for the present corpus, which uses + ASCII-only object keys and integer-only numbers. It is NOT a complete + RFC 8785 implementation. Before adding vectors that exercise: + - non-ASCII member names (JCS sorts by UTF-16 code units, not Python's + default codepoint order - they diverge for characters above U+FFFF); + - numeric edge cases other than non-negative integers (RFC 8785 §3.2.2.3 + reuses ECMA-262 number serialization, which Python's json does not); + - object members containing characters that Python's json escapes + differently from RFC 8785 (forward slash, U+007F, etc.), + replace this with a verified RFC 8785 implementation, otherwise the + generated signatures will diverge from the wire-format expectation. + """ + return json.dumps( + obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def sign(priv: Ed25519PrivateKey, context: str, payload: dict) -> str: + """Sign an Entangled payload with the given context string. + + signature_input = context_string || 0x00 || JCS(payload) + Returns the signature as base64url with no padding. + """ + sig_input = context.encode("ascii") + b"\x00" + jcs(payload) + sig = priv.sign(sig_input) + return b64u(sig) + + +def sha256_b64u(data: bytes) -> str: + """SHA-256 of `data`, formatted as 'sha-256:'.""" + digest = hashlib.sha256(data).digest() + return f"sha-256:{b64u(digest)}" + + +def onion_address(origin_pub: bytes) -> str: + """Tor v3 onion address from a 32-byte Ed25519 public key (rend-spec-v3).""" + version = bytes([0x03]) + checksum = hashlib.sha3_256(b".onion checksum" + origin_pub + version).digest()[:2] + body = origin_pub + checksum + version + return base64.b32encode(body).decode("ascii").lower() + ".onion" + + +def load_bip39_wordlist() -> list[str] | None: + """Load the bundled BIP-39 English wordlist, returning None if absent. + + The corpus PIP-derivation vector is populated only when the wordlist file + is present; absence is non-fatal so the rest of the corpus regenerates. + """ + if not BIP39_WORDLIST_PATH.exists(): + return None + words = BIP39_WORDLIST_PATH.read_text(encoding="ascii").splitlines() + if len(words) != 2048: + raise RuntimeError( + f"BIP-39 wordlist at {BIP39_WORDLIST_PATH} has {len(words)} entries; " + f"expected 2048." + ) + return words + + +def compute_pip(pub_key: bytes, wordlist: list[str]) -> str: + """Derive the 24-word PIP from a 32-byte Ed25519 public key (§05). + + Procedure (BIP-39 over `K_publisher.pub`): + 1. entropy = pub_key (256 bits = 32 bytes). + 2. checksum = first 8 bits of SHA-256(entropy). + 3. bits = entropy || checksum (264 bits). + 4. split into 24 groups of 11 bits, MSB-first. + 5. each group indexes the BIP-39 English wordlist. + 6. join with single ASCII spaces. + """ + if len(pub_key) != 32: + raise ValueError("PIP derivation requires a 32-byte Ed25519 public key.") + if len(wordlist) != 2048: + raise ValueError("BIP-39 wordlist must contain exactly 2048 entries.") + checksum_byte = hashlib.sha256(pub_key).digest()[0] + bits = int.from_bytes(pub_key, "big") << 8 | checksum_byte # 264 bits total + words: list[str] = [] + for i in range(24): + shift = (23 - i) * 11 + idx = (bits >> shift) & 0x7FF + words.append(wordlist[idx]) + return " ".join(words) + + +# --------------------------------------------------------------------------- +# Domain separation context strings (§05) +# --------------------------------------------------------------------------- +CTX_MANIFEST = "ENTANGLED-v1 manifest" +CTX_CONTENT = "ENTANGLED-v1 content" +CTX_TRANSACTION = "ENTANGLED-v1 transaction" + + +# --------------------------------------------------------------------------- +# Document factories +# --------------------------------------------------------------------------- +def make_manifest(*, publisher_priv, publisher_pub, origin_pub, runtime_pub, + issued_at="2026-05-07T00:00:00Z", + next_expected="2026-06-06T00:00:00Z", + updated="2026-05-07T00:00:00Z", + state_policy=None, + not_after: str | None = None, + migration_pointer: dict | None = None) -> dict: + """Build and sign a minimal valid manifest. + + Optional `not_after`: when provided, added as `origin.not_after` (§06). + Optional `migration_pointer`: when provided, added as the top-level + `migration_pointer` field (§06). + """ + if state_policy is None: + state_policy = [] + origin: dict = { + "carrier": "tor-v3", + "address": onion_address(origin_pub), + "origin_pubkey": b64u(origin_pub), + } + if not_after is not None: + origin["not_after"] = not_after + payload: dict = { + "spec_version": "1.0", + "kind": "manifest", + "publisher_pubkey": b64u(publisher_pub), + "origin": origin, + "canary": { + "runtime_pubkey": b64u(runtime_pub), + "issued_at": issued_at, + "next_expected": next_expected, + "statement": "No warrants received.", + }, + "state_policy": state_policy, + "navigation": [], + "min_refresh_interval": 3600, + "updated": updated, + } + if migration_pointer is not None: + payload["migration_pointer"] = migration_pointer + payload["sig"] = sign(publisher_priv, CTX_MANIFEST, payload) + return payload + + +def make_content(*, runtime_priv, path="/articles/first-post", + title="First post", published_at="2026-05-07T00:00:00Z", + blocks=None) -> dict: + """Build and sign a minimal valid content document.""" + if blocks is None: + blocks = [ + { + "kind": "paragraph", + "content": [ + {"kind": "text", "value": "Hello, world.", "marks": []}, + ], + } + ] + payload = { + "spec_version": "1.0", + "kind": "content", + "path": path, + "meta": {"title": title, "published_at": published_at}, + "blocks": blocks, + } + payload["sig"] = sign(runtime_priv, CTX_CONTENT, payload) + return payload + + +def make_transaction(*, runtime_priv, in_response_to="/contact", + submit_body=None, blocks=None, + state_updates=None) -> tuple[dict, dict]: + """Build and sign a transaction document. + + Returns (transaction_doc, submit_body_used). The submit body is needed by + the client to verify request_hash; vectors carrying transactions also + carry the corresponding submit body. + """ + if submit_body is None: + submit_body = { + "fields": {"message": "hello", "name": "alice"}, + "request_state": [], + "request_id": "AAECAwQFBgcICQoLDA0ODw", + } + if blocks is None: + blocks = [ + { + "kind": "feedback", + "variant": "success", + "content": [ + {"kind": "text", "value": "Received.", "marks": []}, + ], + } + ] + if state_updates is None: + state_updates = [] + submit_canonical = jcs(submit_body) + request_hash = sha256_b64u(submit_canonical) + payload = { + "spec_version": "1.0", + "kind": "transaction", + "in_response_to": in_response_to, + "request_id": submit_body["request_id"], + "request_hash": request_hash, + "state_updates": state_updates, + "blocks": blocks, + } + payload["sig"] = sign(runtime_priv, CTX_TRANSACTION, payload) + return payload, submit_body + + +# --------------------------------------------------------------------------- +# Vector emission +# --------------------------------------------------------------------------- +def write_vector(vid: str, body: bytes, *, filename: str = "input.json") -> str: + """Write a vector's raw bytes to corpus/vectors//. + + Returns the relative path stored in the corpus index. + """ + vdir = VECTORS_DIR / vid + vdir.mkdir(parents=True, exist_ok=True) + path = vdir / filename + path.write_bytes(body) + return f"vectors/{vid}/{filename}" + + +def vec(vid: str, kind: str, description: str, spec_refs: list[str], + verdict: str, *, body: bytes | None = None, + body_obj: dict | None = None, diagnostic: str | None = None, + diagnostic_details: dict | None = None, + context: dict | None = None, + extra_files: dict[str, bytes] | None = None) -> dict: + """Build a corpus vector entry and write its files.""" + if body is None: + if body_obj is None: + raise ValueError("either body or body_obj required") + # Serialize without sort_keys so the wire form preserves authoring order. + body = json.dumps(body_obj, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + input_path = write_vector(vid, body) + expected: dict = {"verdict": verdict} + if diagnostic is not None: + expected["diagnostic"] = diagnostic + if diagnostic_details is not None: + expected["diagnostic_details"] = diagnostic_details + entry = { + "id": vid, + "kind": kind, + "description": description, + "spec_refs": spec_refs, + "input": input_path, + "expected": expected, + } + if context is not None: + entry["context"] = context + if extra_files: + for fname, fdata in extra_files.items(): + write_vector(vid, fdata, filename=fname) + entry["extra_files"] = sorted(extra_files.keys()) + return entry + + +# --------------------------------------------------------------------------- +# Vector definitions +# --------------------------------------------------------------------------- +def positive_vectors(keys) -> list[dict]: + """Documents that a conforming v1.0 implementation MUST accept.""" + out: list[dict] = [] + pp, pp_pub = keys["publisher_priv"], keys["publisher_pub"] + rp, rp_pub = keys["runtime_priv"], keys["runtime_pub"] + op, op_pub = keys["origin_priv"], keys["origin_pub"] + + # 001: minimal valid manifest + m = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + ) + out.append(vec( + "001-manifest-valid-minimal", + kind="manifest", + description="Minimal valid manifest signed by K_publisher. Empty state_policy and navigation. Tor v3 origin with derived address.", + spec_refs=["§02", "§05", "§06"], + verdict="accept", + body_obj=m, + context={"fetched_origin_address": m["origin"]["address"]}, + )) + + # 002: valid manifest with state_policy + m2 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + state_policy=[ + { + "namespace": "session", + "key": "auth", + "mode": "request", + "max_size": 512, + "max_lifetime": 86400, + "purpose": "Authenticate submit requests after login.", + }, + { + "namespace": "ui", + "key": "lang", + "mode": "client_only", + "max_size": 32, + "max_lifetime": 7776000, + "purpose": "Remember the chosen language for the user interface.", + }, + ], + ) + out.append(vec( + "002-manifest-valid-state-policy", + kind="manifest", + description="Valid manifest declaring two state_policy entries: one request-mode session token, one client-only language preference.", + spec_refs=["§06", "§07"], + verdict="accept", + body_obj=m2, + context={"fetched_origin_address": m2["origin"]["address"]}, + )) + + # 003: valid content document + c = make_content(runtime_priv=rp) + out.append(vec( + "003-content-valid-minimal", + kind="content", + description="Minimal valid content document with a single paragraph block. Signed by K_runtime authorized by manifest 001.", + spec_refs=["§02", "§03", "§05"], + verdict="accept", + body_obj=c, + context={ + "fetched_path": c["path"], + "expected_runtime_pubkey": b64u(rp_pub), + }, + )) + + # 004: valid content with multiple block kinds + c2 = make_content( + runtime_priv=rp, + path="/articles/blocks-showcase", + title="Block showcase", + blocks=[ + {"kind": "heading", "level": 1, "content": [ + {"kind": "text", "value": "Showcase", "marks": []}, + ]}, + {"kind": "paragraph", "content": [ + {"kind": "text", "value": "An example of ", "marks": []}, + {"kind": "text", "value": "bold", "marks": ["bold"]}, + {"kind": "text", "value": " and ", "marks": []}, + {"kind": "text", "value": "italic", "marks": ["italic"]}, + {"kind": "text", "value": " text.", "marks": []}, + ]}, + {"kind": "list", "ordered": False, "items": [ + [{"kind": "text", "value": "First", "marks": []}], + [{"kind": "text", "value": "Second", "marks": []}], + ]}, + {"kind": "code_block", "language": "rust", + "content": "fn main() {\n println!(\"hi\");\n}"}, + {"kind": "divider"}, + {"kind": "quote", "content": [ + {"kind": "text", "value": "Lorem ipsum.", "marks": []}, + ]}, + ], + ) + out.append(vec( + "004-content-valid-blocks-showcase", + kind="content", + description="Valid content document exercising heading, marked paragraph, unordered list, code_block, divider, and quote. No image; image is exercised separately.", + spec_refs=["§03"], + verdict="accept", + body_obj=c2, + context={ + "fetched_path": c2["path"], + "expected_runtime_pubkey": b64u(rp_pub), + }, + )) + + # 005: valid transaction document + t, sb = make_transaction(runtime_priv=rp) + out.append(vec( + "005-transaction-valid-minimal", + kind="transaction", + description="Minimal valid transaction document with a single feedback block. Carries a request_hash bound to the submit body in extra_files/submit_body.json.", + spec_refs=["§02", "§09"], + verdict="accept", + body_obj=t, + context={ + "submit_path": t["in_response_to"], + "expected_runtime_pubkey": b64u(rp_pub), + "submit_body_path": "vectors/005-transaction-valid-minimal/submit_body.json", + }, + extra_files={ + "submit_body.json": json.dumps( + sb, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8"), + }, + )) + + # 006: valid manifest with origin.not_after declared + m6 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + not_after="2027-05-07T00:00:00Z", + ) + out.append(vec( + "006-manifest-valid-not-after", + kind="manifest", + description=( + "Valid manifest declaring origin.not_after = 2027-05-07T00:00:00Z, one year " + "after canary.issued_at and well within the 5-year ceiling. At clock_now " + "(2026-05-07) the manifest is not yet origin-expired. Exercises the rc.14 " + "origin-not-after schema acceptance and Stage 5 cross-field semantic checks " + "(strictly later than canary.issued_at; not more than 5 years after; SHOULD " + "later than canary.next_expected - satisfied here)." + ), + spec_refs=["§06", "§10"], + verdict="accept", + body_obj=m6, + context={"fetched_origin_address": m6["origin"]["address"]}, + )) + + # 007: valid content document whose seq is above 2^53. + # + # §04 (rc.27): Entangled integers up to 2^63-1 are canonicalized as exact + # decimal across the whole range, overriding the IEEE 754 binary64 + # interpretation for integers above 2^53. seq = 9007199254740993 (2^53+1) + # is the smallest integer with no binary64 representation; an + # implementation that routed it through a double would emit + # 9007199254740992 and produce different canonical bytes (and a failing + # signature). The signature here is computed over the exact-decimal + # canonical form, so a conforming verifier MUST accept it and a + # binary64-rounding one MUST fail. Exercises the §04 integer + # serialization rule at the supra-2^53 boundary. + big_seq = 9007199254740993 # 2**53 + 1 + c7 = { + "spec_version": "1.0", + "kind": "content", + "path": "/articles/large-seq", + "seq": big_seq, + "meta": {"title": "Large seq", "published_at": "2026-05-07T00:00:00Z"}, + "blocks": [ + { + "kind": "paragraph", + "content": [ + {"kind": "text", "value": "Hello, world.", "marks": []}, + ], + } + ], + } + c7["sig"] = sign(rp, CTX_CONTENT, c7) + out.append(vec( + "007-content-valid-large-seq", + kind="content", + description=( + "Valid content document whose seq = 9007199254740993 (2^53 + 1), " + "the smallest integer with no IEEE 754 binary64 representation. " + "Per §04 integer serialization, Entangled integers up to 2^63-1 " + "are canonicalized as exact decimal across the whole range, " + "overriding the binary64 interpretation above 2^53. The K_runtime " + "signature is computed over the exact-decimal canonical form. A " + "conforming verifier MUST accept; an implementation that routes " + "the seq through a binary64 double serializes 9007199254740992 " + "and fails signature verification. Signed by K_runtime authorized " + "by manifest 001." + ), + spec_refs=["§02", "§04", "§05"], + verdict="accept", + body_obj=c7, + context={ + "fetched_path": c7["path"], + "expected_runtime_pubkey": b64u(rp_pub), + }, + )) + + return out + + +def negative_vectors(keys) -> list[dict]: + """Documents that a conforming v1.0 implementation MUST reject, with the + specific diagnostic listed.""" + out: list[dict] = [] + pp, pp_pub = keys["publisher_priv"], keys["publisher_pub"] + rp, rp_pub = keys["runtime_priv"], keys["runtime_pub"] + op, op_pub = keys["origin_priv"], keys["origin_pub"] + + # ---- input: BOM, bad UTF-8 ---- + m = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + ) + m_bytes = json.dumps(m, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + out.append(vec( + "100-input-bom", + kind="manifest", + description="Otherwise-valid manifest preceded by a UTF-8 BOM (EF BB BF). Must be rejected at stage 2 input checks.", + spec_refs=["§04"], + verdict="reject", + diagnostic="E_INPUT_BOM", + body=b"\xEF\xBB\xBF" + m_bytes, + )) + out.append(vec( + "101-input-bad-utf8", + kind="manifest", + description="Body is not valid UTF-8: contains a lone 0xFE byte that is not part of any UTF-8 sequence. Must be rejected at stage 2.", + spec_refs=["§04"], + verdict="reject", + diagnostic="E_INPUT_UTF8", + body=b'{"spec_version":"1.0","kind":"manifest","x":"\xFE"}', + )) + + # ---- parse: duplicate keys ---- + out.append(vec( + "110-parse-duplicate-keys", + kind="content", + description="Content document with a duplicate top-level member name (\"path\" appears twice). Must be rejected at stage 3 with E_PARSE_DUPLICATE_KEY before schema validation.", + spec_refs=["§04"], + verdict="reject", + diagnostic="E_PARSE_DUPLICATE_KEY", + body=b'{"spec_version":"1.0","kind":"content","path":"/x","path":"/y","meta":{"title":"t","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"divider"}],"sig":"' + (b"A" * 86) + b'"}', + )) + + # ---- kind / spec_version ---- + out.append(vec( + "120-spec-version-wrong", + kind="manifest", + description="Document declaring spec_version \"1.1\". A v1.0 client must reject with E_KIND_SPEC_VERSION before schema validation.", + spec_refs=["§02", "§11"], + verdict="reject", + diagnostic="E_KIND_SPEC_VERSION", + body=b'{"spec_version":"1.1","kind":"manifest","sig":"' + (b"A" * 86) + b'"}', + )) + out.append(vec( + "121-kind-unknown", + kind="manifest", + description="Document whose kind is \"unknown\" (not one of manifest/content/transaction). Rejected at stage 4 with E_KIND_UNKNOWN.", + spec_refs=["§02", "§11"], + verdict="reject", + diagnostic="E_KIND_UNKNOWN", + body=b'{"spec_version":"1.0","kind":"unknown","sig":"' + (b"A" * 86) + b'"}', + )) + + # ---- schema: unknown field, missing required, null value ---- + bad = dict(m) + bad["unexpected_field"] = "x" + # need to re-sign would be wrong (signature wouldn't match this payload anyway, + # because we can't sign a document our schema rejects from inside the + # generator). Use the original signature; the test exercises stage 5 schema + # rejection ahead of stage 6 signature verification. + out.append(vec( + "130-schema-unknown-field", + kind="manifest", + description="Manifest with an extra top-level field \"unexpected_field\". Closed-schema discipline rejects at stage 5 with E_SCHEMA_UNKNOWN_FIELD before signature verification.", + spec_refs=["§02", "§06"], + verdict="reject", + diagnostic="E_SCHEMA_UNKNOWN_FIELD", + body_obj=bad, + )) + + bad2 = {k: v for k, v in m.items() if k != "min_refresh_interval"} + out.append(vec( + "131-schema-missing-required", + kind="manifest", + description="Manifest with required field min_refresh_interval omitted. Rejected at stage 5 with E_SCHEMA_REQUIRED_FIELD.", + spec_refs=["§06"], + verdict="reject", + diagnostic="E_SCHEMA_REQUIRED_FIELD", + body_obj=bad2, + )) + + m_null_nav = dict(m) + m_null_nav["navigation"] = None + m_null_nav["sig"] = "A" * 86 # placeholder; stage 5 fails before stage 6 + out.append(vec( + "132-schema-null-value", + kind="manifest", + description=( + "Manifest where navigation is null. All other required fields " + "are present and well-formed; only the null literal triggers " + "stage 5 rejection. E_SCHEMA_NULL_VALUE." + ), + spec_refs=["§04", "§06"], + verdict="reject", + diagnostic="E_SCHEMA_NULL_VALUE", + body_obj=m_null_nav, + context={"fetched_origin_address": m_null_nav["origin"]["address"]}, + )) + + # Invalid block kind in content document + c_bad_block = make_content( + runtime_priv=rp, + path="/articles/bad-block", + title="Bad block", + blocks=[{"kind": "marquee", "content": "scrolling text"}], + ) + out.append(vec( + "133-schema-block-kind-unknown", + kind="content", + description=( + "Content document with a block whose kind is \"marquee\", a " + "syntactically valid slug not in the enumerated block kinds " + "(§03). Stage 5 schema rejection. E_SCHEMA_ENUM_VIOLATION." + ), + spec_refs=["§03", "§11"], + verdict="reject", + diagnostic="E_SCHEMA_ENUM_VIOLATION", + body_obj=c_bad_block, + context={ + "fetched_path": c_bad_block["path"], + "expected_runtime_pubkey": b64u(rp_pub), + }, + )) + + # ---- numeric grammar: float, big int ---- + out.append(vec( + "140-numeric-float", + kind="manifest", + description="Manifest where min_refresh_interval has a float-shape token (3600.0). The strict integer grammar rejects floats lexically. E_SCHEMA_NON_INTEGER.", + spec_refs=["§04"], + verdict="reject", + diagnostic="E_SCHEMA_NON_INTEGER", + body=b'{"spec_version":"1.0","kind":"manifest","min_refresh_interval":3600.0,"sig":"' + (b"A" * 86) + b'"}', + )) + out.append(vec( + "141-numeric-exponent", + kind="manifest", + description="Manifest where min_refresh_interval is written in exponent form (3.6e3). Integer grammar rejects exponents. E_SCHEMA_NON_INTEGER.", + spec_refs=["§04"], + verdict="reject", + diagnostic="E_SCHEMA_NON_INTEGER", + body=b'{"spec_version":"1.0","kind":"manifest","min_refresh_interval":3.6e3,"sig":"' + (b"A" * 86) + b'"}', + )) + m_overflow = dict(m) + m_overflow["min_refresh_interval"] = 9223372036854775808 # 2**63 + m_overflow["sig"] = "A" * 86 + out.append(vec( + "142-numeric-overflow", + kind="manifest", + description=( + "Manifest where min_refresh_interval is 9223372036854775808 " + "(= 2^63), one above the protocol's 64-bit signed integer " + "cap. All other required fields are present and well-formed. " + "E_SCHEMA_NON_INTEGER." + ), + spec_refs=["§04", "§06"], + verdict="reject", + diagnostic="E_SCHEMA_NON_INTEGER", + body_obj=m_overflow, + context={"fetched_origin_address": m_overflow["origin"]["address"]}, + )) + + # ---- 143-submit-budget-state-overflow (Stage 5, E_SUBMIT_BUDGET) ---- + # Manifest whose state_policy declares 32 request-mode entries each + # with max_size = 2048 bytes. The aggregate worst-case encoded wire + # contribution to the submit body's request_state array counts the + # value at its raw max_size (2048 UTF-8 bytes, no JSON-escape + # expansion, per §07 max_size as a raw UTF-8 byte length): per entry + # 36 envelope bytes + 2 namespace bytes + 3 key bytes + 2048 value + # bytes = 2089 bytes, times 32 entries, plus 31 inter-entry commas = + # 66879 bytes, well above the state_budget of 53248 bytes defined in + # §09 ("Submit body budget partition"). Per §07 "Submit budget + # satisfiability", the manifest is rejected at Stage 5 schema + # validation as E_SUBMIT_BUDGET with details.component = "state". The + # manifest is signed correctly and otherwise valid; the + # satisfiability violation is the only live violation at Stage 5. + # The escape-sensitive per-value wire boundary (a value whose + # JSON-escaped wire length exceeds its raw max_size) lives in the + # deferred runtime E_STATE_TRANSMIT_BUDGET path, not in this Stage 5 + # envelope check; see corpus/README.md. + state_policy_overflow = [ + { + "namespace": "ns", + "key": f"k{i:02d}", + "mode": "request", + "max_size": 2048, + "max_lifetime": 86400, + "purpose": "Aggregate overflow probe.", + } + for i in range(32) + ] + m_143 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + state_policy=state_policy_overflow, + ) + out.append(vec( + "143-submit-budget-state-overflow", + kind="manifest", + description=( + "Manifest whose state_policy declares 32 request-mode " + "entries each with max_size = 2048 bytes. Aggregate " + "worst-case encoded wire contribution to request_state is " + "66879 bytes (32 entries * (2048 raw value bytes + 41 " + "envelope bytes: 36 fixed + 2 namespace + 3 key) + 31 " + "commas), exceeding the state_budget of 53248 bytes defined " + "in §09 ('Submit body budget partition') by 13631 bytes. The " + "value is counted at its raw max_size (UTF-8 byte length, no " + "JSON-escape expansion) per §07 max_size. Per §07 'Submit " + "budget satisfiability', rejected at Stage 5 schema " + "validation as E_SUBMIT_BUDGET with details.component = " + "'state'. Manifest is signed correctly; the satisfiability " + "violation is the only live Stage 5 violation." + ), + spec_refs=["§07", "§09", "§11"], + verdict="reject", + diagnostic="E_SUBMIT_BUDGET", + diagnostic_details={ + "component": "state", + "declared_bytes": 66879, + "budget_bytes": 53248, + }, + body_obj=m_143, + context={"fetched_origin_address": m_143["origin"]["address"]}, + )) + + # ---- signature: modified payload, wrong length ---- + m_tamper = dict(m) + # Modify a non-sig field after signing. The signature no longer matches. + m_tamper["min_refresh_interval"] = m["min_refresh_interval"] + 1 + out.append(vec( + "150-sig-modified-payload", + kind="manifest", + description="Otherwise-valid manifest whose min_refresh_interval was changed after signing. The wire signature no longer verifies. E_SIG_VERIFICATION.", + spec_refs=["§05"], + verdict="reject", + diagnostic="E_SIG_VERIFICATION", + body_obj=m_tamper, + context={"fetched_origin_address": m_tamper["origin"]["address"]}, + )) + + # Sig field length: 43 chars instead of the canonical 86. Stage 5 §04 + # declared-length check fires before stage 6 signature decoding (§10 + # first-failing-stage rule), so the diagnostic is E_SCHEMA_FIELD_SYNTAX, + # not E_SIG_MALFORMED. + short_sig = b64u(b"\x00" * 32) # 43 chars + m_short = dict(m) + m_short["sig"] = short_sig + out.append(vec( + "151-sig-syntax-length", + kind="manifest", + description=( + "Manifest whose sig field is 43 ASCII characters instead of the " + "canonical 86. §04 declared-length check at stage 5 rejects with " + "E_SCHEMA_FIELD_SYNTAX before stage 6 signature decoding fires " + "(§10 first-failing-stage precedence)." + ), + spec_refs=["§04", "§02"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_SYNTAX", + body_obj=m_short, + context={"fetched_origin_address": m_short["origin"]["address"]}, + )) + + # Non-canonical S: take the valid signature from manifest m, replace S + # with S + L. The resulting signature verifies under cofactored rules but + # is rejected under the strict profile (§05). + real_sig = b64u_decode(m["sig"]) + nc_sig = non_canonical_s(real_sig) + m_nc = dict(m) + m_nc["sig"] = b64u(nc_sig) + out.append(vec( + "152-sig-non-canonical-s", + kind="manifest", + description="Manifest with a signature whose S component is non-canonical (S' = S + L >= L). The signature would verify under cofactored Ed25519, but the strict profile (§05) rejects non-canonical S. E_SIG_VERIFICATION.", + spec_refs=["§05"], + verdict="reject", + diagnostic="E_SIG_VERIFICATION", + body_obj=m_nc, + context={"fetched_origin_address": m_nc["origin"]["address"]}, + )) + + # Small-order public key (identity). The strict profile rejects the + # public key before signature verification; the vector replaces both + # publisher_pubkey and the sig with a placeholder. Even with a forged + # signature, the public-key rejection takes precedence. + m_smallorder = dict(m) + m_smallorder["publisher_pubkey"] = b64u(SMALL_ORDER_A) + m_smallorder["sig"] = b64u(b"\x00" * 64) + out.append(vec( + "153-sig-small-order-pubkey", + kind="manifest", + description="Manifest where publisher_pubkey is the encoded identity point (small-order, order 1). The strict profile (§05) rejects small-order public keys before signature verification; E_SIG_VERIFICATION.", + spec_refs=["§05"], + verdict="reject", + diagnostic="E_SIG_VERIFICATION", + body_obj=m_smallorder, + context={"fetched_origin_address": m_smallorder["origin"]["address"]}, + )) + + # ---- base64url strictness ---- + # padded sig + m_padded = dict(m) + real_sig_b = b64u_decode(m["sig"]) + m_padded["sig"] = base64.urlsafe_b64encode(real_sig_b).decode("ascii") # keeps "=" padding - no rstrip + out.append(vec( + "160-base64url-padded", + kind="manifest", + description="Manifest whose sig field carries '=' padding. Strict base64url decoding rejects with E_SCHEMA_FIELD_SYNTAX before signature verification.", + spec_refs=["§04", "§02"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_SYNTAX", + body_obj=m_padded, + context={"fetched_origin_address": m_padded["origin"]["address"]}, + )) + + # standard alphabet (+/) instead of url-safe (-_) + m_stdalpha = dict(m) + std_b64 = base64.b64encode(real_sig_b).rstrip(b"=").decode("ascii") + if "+" not in std_b64 and "/" not in std_b64: + # extremely unlikely with random 64-byte sig but handle gracefully + std_b64 = std_b64[:-1] + "+" + m_stdalpha["sig"] = std_b64 + out.append(vec( + "161-base64url-standard-alphabet", + kind="manifest", + description="Manifest whose sig field uses the standard base64 alphabet (+ and /) instead of the URL-safe alphabet (- and _). Rejected with E_SCHEMA_FIELD_SYNTAX.", + spec_refs=["§04"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_SYNTAX", + body_obj=m_stdalpha, + context={"fetched_origin_address": m_stdalpha["origin"]["address"]}, + )) + + # whitespace in sig + m_ws = dict(m) + m_ws["sig"] = m["sig"][:43] + " " + m["sig"][43:] + out.append(vec( + "162-base64url-whitespace", + kind="manifest", + description="Manifest whose sig field contains an embedded space character. Strict base64url rejects whitespace; E_SCHEMA_FIELD_SYNTAX.", + spec_refs=["§04"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_SYNTAX", + body_obj=m_ws, + context={"fetched_origin_address": m_ws["origin"]["address"]}, + )) + + # ---- binding: path mismatch, /manifest.json reservation, request_hash ---- + c = make_content(runtime_priv=rp, path="/articles/foo") + out.append(vec( + "170-bind-path-mismatch", + kind="content", + description="Otherwise-valid content document whose path field is /articles/foo, fetched from /articles/bar. Stage 9 path binding rejects with E_BIND_PATH.", + spec_refs=["§02", "§10"], + verdict="reject", + diagnostic="E_BIND_PATH", + body_obj=c, + context={ + "fetched_path": "/articles/bar", + "expected_runtime_pubkey": b64u(rp_pub), + }, + )) + + # /manifest.json as content path - schema-level rejection (rc.6 reservation) + c_manifest_path = make_content(runtime_priv=rp, path="/manifest.json") + out.append(vec( + "171-bind-reserved-manifest-path", + kind="content", + description="Content document declaring path /manifest.json. The path is reserved for manifest fetches and the schema rejects it with E_SCHEMA_FIELD_SYNTAX.", + spec_refs=["§02", "§09"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_SYNTAX", + body_obj=c_manifest_path, + context={ + "fetched_path": "/manifest.json", + "expected_runtime_pubkey": b64u(rp_pub), + }, + )) + + # transaction with mismatched request_hash + t, sb = make_transaction(runtime_priv=rp) + # Tamper the recorded submit body so the locally-computed request_hash + # differs from the one in the (still valid) transaction. + sb_tampered = dict(sb) + sb_tampered["fields"] = {"message": "TAMPERED", "name": "alice"} + out.append(vec( + "172-bind-request-hash-mismatch", + kind="transaction", + description="Transaction document whose request_hash matches the original submit body, but the client's recorded submit body has been tampered (fields.message changed). Stage 9 rejects with E_BIND_REQUEST_HASH.", + spec_refs=["§02", "§09"], + verdict="reject", + diagnostic="E_BIND_REQUEST_HASH", + body_obj=t, + context={ + "submit_path": t["in_response_to"], + "expected_runtime_pubkey": b64u(rp_pub), + "submit_body_path": "vectors/172-bind-request-hash-mismatch/submit_body.json", + }, + extra_files={ + "submit_body.json": json.dumps( + sb_tampered, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8"), + }, + )) + + # ---- canary: equal issued_at conflict ---- + m_alt = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=keys["runtime_pub_2"], + # same issued_at as 001 + ) + out.append(vec( + "180-canary-equal-issued-at-conflict", + kind="manifest", + description="Two manifests with the same canary.issued_at and the same K_publisher.pub but different runtime_pubkey. Once 001 is verified and retained, observing this manifest at the same issued_at must produce E_CANARY_CONFLICT.", + spec_refs=["§08"], + verdict="reject", + diagnostic="E_CANARY_CONFLICT", + body_obj=m_alt, + context={ + "fetched_origin_address": m_alt["origin"]["address"] + , + "previously_verified": "vectors/001-manifest-valid-minimal/input.json", + }, + )) + + # ---- additional sig strictness: non-canonical R, non-canonical A ---- + # Non-canonical R: replace R with an encoding whose y portion equals + # 2^255 - 1, which exceeds the Ed25519 prime p = 2^255 - 19. RFC 8032 + # requires y < p; the strict profile rejects this encoding before the + # cryptographic verification equation is evaluated. S is left unchanged + # at the value from the original valid signature on m, but the R + # rejection takes precedence. + real_sig_bytes = b64u_decode(m["sig"]) + nc_r_sig = non_canonical_r_encoding() + real_sig_bytes[32:] + m_nc_r = dict(m) + m_nc_r["sig"] = b64u(nc_r_sig) + out.append(vec( + "154-sig-non-canonical-r", + kind="manifest", + description="Manifest whose signature R has a non-canonical compressed point encoding (y >= p). The strict profile (§05) rejects non-canonical encodings of R independently of any verification equation. E_SIG_VERIFICATION.", + spec_refs=["§05"], + verdict="reject", + diagnostic="E_SIG_VERIFICATION", + body_obj=m_nc_r, + context={"fetched_origin_address": m_nc_r["origin"]["address"]}, + )) + + # Non-canonical A: replace publisher_pubkey with the same all-0xff + # encoding; the strict profile (§05) rejects A whose y portion exceeds + # the field prime, before any signature check. The sig field is left + # at the original valid value; the A rejection happens first. + m_nc_a = dict(m) + m_nc_a["publisher_pubkey"] = b64u(non_canonical_r_encoding()) + out.append(vec( + "155-sig-non-canonical-a", + kind="manifest", + description="Manifest whose publisher_pubkey is a non-canonical Ed25519 point encoding (y >= p). The strict profile (§05) rejects non-canonical encodings of A before signature verification. E_SIG_VERIFICATION.", + spec_refs=["§05"], + verdict="reject", + diagnostic="E_SIG_VERIFICATION", + body_obj=m_nc_a, + context={"fetched_origin_address": m_nc_a["origin"]["address"]}, + )) + + # ---- canary: anti-downgrade on issued_at ---- + # A manifest with strictly older issued_at than 001's (2026-05-07). + # The vector context references 001 as previously verified, so the + # client's anti-downgrade rule must reject this manifest as a downgrade. + m_old = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + issued_at="2026-04-01T00:00:00Z", + next_expected="2026-05-01T00:00:00Z", + updated="2026-04-01T00:00:00Z", + ) + out.append(vec( + "181-canary-issued-at-downgrade", + kind="manifest", + description="Manifest with canary.issued_at strictly older than the issued_at of a previously verified manifest (001) for the same K_publisher.pub. The client's anti-downgrade rule (§08) rejects with E_CANARY_DOWNGRADE.", + spec_refs=["§08"], + verdict="reject", + diagnostic="E_CANARY_DOWNGRADE", + body_obj=m_old, + context={ + "fetched_origin_address": m_old["origin"]["address"], + "previously_verified": "vectors/001-manifest-valid-minimal/input.json", + }, + )) + + # ---- Unicode normalization: NFD canary.statement ---- + # The publisher must encode user-visible strings in NFC (§04). A + # canary.statement using decomposed combining marks (NFD) instead of + # precomposed characters (NFC) is rejected at schema validation. The + # statement "Café" in NFD is "Cafe" + U+0301 (combining acute). + nfd_statement = "Cafe\u0301" # NFD form of "Café" + # Build a manifest with this statement directly. We sign it (the JCS bytes + # over the NFD payload differ from the NFC equivalent), so the signature + # itself is valid; the rejection is at schema validation, before sig check. + m_nfd_payload = { + "spec_version": "1.0", + "kind": "manifest", + "publisher_pubkey": b64u(pp_pub), + "origin": { + "carrier": "tor-v3", + "address": onion_address(op_pub), + "origin_pubkey": b64u(op_pub), + }, + "canary": { + "runtime_pubkey": b64u(rp_pub), + "issued_at": "2026-05-07T00:00:00Z", + "next_expected": "2026-06-06T00:00:00Z", + "statement": nfd_statement, + }, + "state_policy": [], + "navigation": [], + "min_refresh_interval": 3600, + "updated": "2026-05-07T00:00:00Z", + } + m_nfd_payload["sig"] = sign(pp, CTX_MANIFEST, m_nfd_payload) + out.append(vec( + "190-unicode-nfd-statement", + kind="manifest", + description="Manifest whose canary.statement contains a decomposed combining mark (NFD) rather than the precomposed NFC form. Per §04, user-visible strings must be in NFC. Rejected at schema validation with E_SCHEMA_FIELD_SYNTAX before signature verification.", + spec_refs=["§04", "§08"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_SYNTAX", + body_obj=m_nfd_payload, + context={"fetched_origin_address": m_nfd_payload["origin"]["address"]}, + )) + + # ---- 200: migration scenario, successor manifest origin-expired ---- + # + # Announcing manifest at the original origin (op_pub) carries a + # migration_pointer to a successor origin (op_pub_2). The successor + # manifest is signed correctly by the same K_publisher and binds correctly + # to the successor address, but its own origin.not_after has already + # passed at clock_now (2026-05-07). The successor therefore fails Stage 9 + # in isolation with E_ORIGIN_EXPIRED. Per §10 "Successor verification" and + # §11, the migration is rejected under E_MIGRATION_MISMATCH with + # mismatch_field="successor_stage9_failure" and underlying_diagnostic_code + # ="E_ORIGIN_EXPIRED". The announcing manifest is itself accepted at its + # origin (verdict reject here refers to the migration adoption outcome). + op_pub_2 = keys["origin_pub_2"] + successor_address = onion_address(op_pub_2) + successor = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub_2, runtime_pub=rp_pub, + issued_at="2026-04-01T00:00:00Z", + next_expected="2026-05-01T00:00:00Z", + updated="2026-04-01T00:00:00Z", + not_after="2026-05-01T00:00:00Z", # past relative to clock_now + ) + successor_bytes = json.dumps( + successor, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + announcing = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + migration_pointer={ + "successor_origin": { + "carrier": "tor-v3", + "address": successor_address, + "origin_pubkey": b64u(op_pub_2), + }, + "announced_at": "2026-05-07T00:00:00Z", + }, + ) + out.append(vec( + "200-migration-successor-origin-expired", + kind="manifest", + description=( + "Migration scenario exercising the rc.15 successor_stage9_failure path. " + "The announcing manifest at the original origin is itself valid and accepted " + "in isolation; it carries a migration_pointer to a successor origin. The " + "successor manifest (in extra_files/successor_manifest.json) is signed " + "correctly by the same K_publisher and binds to the successor address per " + "the Tor v3 derivation rule, but its own origin.not_after has already " + "passed at clock_now (2026-05-07). The successor fails Stage 9 in isolation " + "with E_ORIGIN_EXPIRED. Per §10 'Successor verification', the migration " + "is rejected under E_MIGRATION_MISMATCH; per §11, details.mismatch_field " + "= 'successor_stage9_failure' and details.underlying_diagnostic_code = " + "'E_ORIGIN_EXPIRED'. The reject verdict here refers to the migration " + "adoption outcome, not to the announcing manifest itself." + ), + spec_refs=["§06", "§10", "§11"], + verdict="reject", + diagnostic="E_MIGRATION_MISMATCH", + diagnostic_details={ + "mismatch_field": "successor_stage9_failure", + "underlying_diagnostic_code": "E_ORIGIN_EXPIRED", + }, + body_obj=announcing, + context={ + "fetched_origin_address": announcing["origin"]["address"], + "successor_origin_address": successor_address, + "successor_manifest_path": "vectors/200-migration-successor-origin-expired/successor_manifest.json", + }, + extra_files={ + "successor_manifest.json": successor_bytes, + }, + )) + + # ===================================================================== + # rc.18 Phase-1 additions: pipeline coverage within the existing + # vector schema. These vectors target §11 diagnostic codes that had + # zero coverage in the rc.17 corpus. Each is constructed so that + # the diagnostic-relevant violation is the only live violation at the + # first failing pipeline stage (corpus isolation rule, see README). + # ===================================================================== + + # ---- 102-input-byte-cap (Stage 2, E_INPUT_BYTE_CAP) ---- + # Manifest-shaped body padded past the 64 KiB Stage 2 byte cap. + # Stage 2 fires before Stage 3 JSON parsing, so the JSON well-formedness + # below the cap is irrelevant. + out.append(vec( + "102-input-byte-cap", + kind="manifest", + description=( + "Manifest-shaped body padded past the 64 KiB byte cap for " + "manifests (§02). Stage 2 input check fires before Stage 3 JSON " + "parsing; well-formedness of the JSON below the cap is therefore " + "irrelevant. E_INPUT_BYTE_CAP." + ), + spec_refs=["§02", "§04"], + verdict="reject", + diagnostic="E_INPUT_BYTE_CAP", + body=( + b'{"spec_version":"1.0","kind":"manifest","_pad":"' + + (b"A" * 70000) + + b'"}' + ), + )) + + # ---- 111-parse-nesting-depth (Stage 3, E_PARSE_NESTING_DEPTH) ---- + # Nested JSON array of depth 20 exceeds the 16-level Stage 3 cap. + out.append(vec( + "111-parse-nesting-depth", + kind="manifest", + description=( + "Manifest body containing a 20-level-deep nested array, " + "exceeding the 16-level Stage 3 nesting cap (§04). The field " + "below the cap is irrelevant; Stage 3 fires before Stage 5. " + "E_PARSE_NESTING_DEPTH." + ), + spec_refs=["§04"], + verdict="reject", + diagnostic="E_PARSE_NESTING_DEPTH", + body=( + b'{"spec_version":"1.0","kind":"manifest","_nest":' + + b"[" * 20 + b"0" + b"]" * 20 + + b',"sig":"' + (b"A" * 86) + b'"}' + ), + )) + + # ---- 112-parse-string-length (Stage 3, E_PARSE_STRING_LENGTH) ---- + # Content document containing a code_block whose content is a string + # one byte above the 100 KiB Stage 3 string cap. Body sits well under + # the 1 MiB content byte cap so Stage 2 passes. + long_str = b"x" * (100 * 1024 + 1) # 102401 bytes + out.append(vec( + "112-parse-string-length", + kind="content", + description=( + "Content document containing a single string of 102401 ASCII " + "bytes, one byte above the 100 KiB Stage 3 string cap (§04). " + "Body remains under the 1 MiB content byte cap so Stage 2 " + "passes and Stage 3 fires. E_PARSE_STRING_LENGTH." + ), + spec_refs=["§04"], + verdict="reject", + diagnostic="E_PARSE_STRING_LENGTH", + body=( + b'{"spec_version":"1.0","kind":"content","path":"/x","meta":' + b'{"title":"t","published_at":"2026-05-07T00:00:00Z"},' + b'"blocks":[{"kind":"code_block","language":"text","content":"' + + long_str + b'"}],"sig":"' + (b"A" * 86) + b'"}' + ), + )) + + # ---- 113-parse-array-length (Stage 3, E_PARSE_ARRAY_LENGTH) ---- + # Content document whose blocks array contains 10001 entries, one above + # the 10000-element Stage 3 array cap. Element shapes are irrelevant; + # Stage 3 fires before Stage 5 schema validation. + out.append(vec( + "113-parse-array-length", + kind="content", + description=( + "Content document whose blocks array contains 10001 entries, " + "one above the 10000-element Stage 3 array cap (§04). Element " + "shapes are irrelevant; Stage 3 fires before Stage 5. " + "E_PARSE_ARRAY_LENGTH." + ), + spec_refs=["§04"], + verdict="reject", + diagnostic="E_PARSE_ARRAY_LENGTH", + body=( + b'{"spec_version":"1.0","kind":"content","path":"/x","meta":' + b'{"title":"t","published_at":"2026-05-07T00:00:00Z"},' + b'"blocks":[' + b"{}," * 10000 + b"{}]" + + b',"sig":"' + (b"A" * 86) + b'"}' + ), + )) + + # ---- 114-parse-object-keys (Stage 3, E_PARSE_OBJECT_KEYS) ---- + # Content document whose meta object contains 257 members, one above + # the 256-key Stage 3 object cap. Member names beyond the meta schema + # are irrelevant; Stage 3 fires before Stage 5. + extra_keys = b",".join(b'"k%d":0' % i for i in range(255)) + out.append(vec( + "114-parse-object-keys", + kind="content", + description=( + "Content document whose meta object contains 257 members, one " + "above the 256-key Stage 3 object cap (§04). Member names " + "beyond the meta schema are irrelevant; Stage 3 fires before " + "Stage 5. E_PARSE_OBJECT_KEYS." + ), + spec_refs=["§04"], + verdict="reject", + diagnostic="E_PARSE_OBJECT_KEYS", + body=( + b'{"spec_version":"1.0","kind":"content","path":"/x","meta":' + b'{"title":"t","published_at":"2026-05-07T00:00:00Z",' + + extra_keys + + b'},"blocks":[{"kind":"divider"}],"sig":"' + + (b"A" * 86) + b'"}' + ), + )) + + # ---- 115-parse-json-malformed (Stage 3, E_PARSE_JSON) ---- + # Body containing a trailing comma followed by a missing value; not + # parseable as JSON. + out.append(vec( + "115-parse-json-malformed", + kind="manifest", + description=( + "Body is not parseable as JSON: trailing comma followed by a " + "missing value at \"sig\". Stage 3 JSON parsing rejects with " + "E_PARSE_JSON." + ), + spec_refs=["§04"], + verdict="reject", + diagnostic="E_PARSE_JSON", + body=b'{"spec_version":"1.0","kind":"manifest","sig":,}', + )) + + # ---- 122-kind-missing-fields (Stage 4, E_KIND_MISSING_FIELDS) ---- + # Document with the top-level sig field omitted. spec_version and kind + # are present and well-formed, but sig - one of the three top-level + # required fields per §02 - is absent. Stage 4 detects this before + # Stage 5 schema would also flag it. + out.append(vec( + "122-kind-missing-fields", + kind="manifest", + description=( + "Document with the top-level sig field omitted. spec_version " + "and kind are present and well-formed, but sig - one of the " + "three top-level required fields per §02 - is absent. Stage 4 " + "kind discrimination fires E_KIND_MISSING_FIELDS." + ), + spec_refs=["§02", "§11"], + verdict="reject", + diagnostic="E_KIND_MISSING_FIELDS", + body=b'{"spec_version":"1.0","kind":"manifest"}', + )) + + # ---- 134-schema-field-type (Stage 5, E_SCHEMA_FIELD_TYPE) ---- + # Manifest where min_refresh_interval is the string "3600" instead of + # a non-negative integer. Stage 5 fires before Stage 6 signature + # verification, so the residual signature is irrelevant. + m_field_type = dict(m) + m_field_type["min_refresh_interval"] = "3600" + out.append(vec( + "134-schema-field-type", + kind="manifest", + description=( + "Manifest where min_refresh_interval is the string \"3600\" " + "instead of a non-negative integer. Stage 5 closed-schema " + "validation fires before Stage 6 signature verification " + "(§10 first-failing-stage). E_SCHEMA_FIELD_TYPE." + ), + spec_refs=["§06", "§11"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_TYPE", + body_obj=m_field_type, + context={"fetched_origin_address": m_field_type["origin"]["address"]}, + )) + + # ---- 135-schema-field-range (Stage 5, E_SCHEMA_FIELD_RANGE) ---- + # Content document with a heading block whose level is 7, outside the + # [1..6] range required by §03. + c_bad_range = make_content( + runtime_priv=rp, + path="/articles/bad-range", + title="Bad range", + blocks=[{ + "kind": "heading", + "level": 7, + "content": [ + {"kind": "text", "value": "Too deep", "marks": []}, + ], + }], + ) + out.append(vec( + "135-schema-field-range", + kind="content", + description=( + "Content document containing a heading block whose level is " + "7, outside the [1..6] range required by §03. Stage 5 schema " + "rejects with E_SCHEMA_FIELD_RANGE before Stage 6 signature " + "verification." + ), + spec_refs=["§03", "§11"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_RANGE", + body_obj=c_bad_range, + context={ + "fetched_path": c_bad_range["path"], + "expected_runtime_pubkey": b64u(rp_pub), + }, + )) + + # ---- 136-schema-block-not-permitted (Stage 5, E_SCHEMA_BLOCK_NOT_PERMITTED) ---- + # Transaction document whose blocks contains a submit_form block. + # submit_form is permitted only in content documents per §03's + # "Block usage by document kind" table. + sb_136 = { + "fields": {}, + "request_state": [], + "request_id": "AAECAwQFBgcICQoLDA0ODw", + } + t_136, _ = make_transaction( + runtime_priv=rp, + submit_body=sb_136, + blocks=[{ + "kind": "submit_form", + "label": [ + {"kind": "text", "value": "Send a message", "marks": []}, + ], + "submit_to": "/contact", + "fields": [ + { + "kind": "textarea", + "name": "message", + "label": "Message", + "required": True, + "max_length": 1000, + } + ], + "submit_label": "Send", + }], + ) + out.append(vec( + "136-schema-block-not-permitted", + kind="transaction", + description=( + "Transaction document whose blocks array contains a " + "submit_form block. submit_form is permitted only in content " + "documents per §03 \"Block usage by document kind\". Stage 5 " + "schema rejects with E_SCHEMA_BLOCK_NOT_PERMITTED." + ), + spec_refs=["§02", "§03"], + verdict="reject", + diagnostic="E_SCHEMA_BLOCK_NOT_PERMITTED", + body_obj=t_136, + context={ + "submit_path": t_136["in_response_to"], + "expected_runtime_pubkey": b64u(rp_pub), + "submit_body_path": "vectors/136-schema-block-not-permitted/submit_body.json", + }, + extra_files={ + "submit_body.json": json.dumps( + sb_136, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8"), + }, + )) + + # ---- 137-schema-duplicate-entry (Stage 5, E_SCHEMA_DUPLICATE_ENTRY) ---- + # Manifest whose state_policy declares two entries with identical + # (namespace, key). §06 requires (namespace, key) uniqueness across + # state_policy entries. + m_dup_policy = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + state_policy=[ + { + "namespace": "session", + "key": "auth", + "mode": "request", + "max_size": 512, + "max_lifetime": 86400, + "purpose": "First entry.", + }, + { + "namespace": "session", + "key": "auth", + "mode": "client_only", + "max_size": 256, + "max_lifetime": 7776000, + "purpose": "Duplicate (namespace, key).", + }, + ], + ) + out.append(vec( + "137-schema-duplicate-entry", + kind="manifest", + description=( + "Manifest whose state_policy contains two entries with " + "identical (namespace, key) = (\"session\", \"auth\"). §06 " + "requires (namespace, key) uniqueness across state_policy " + "entries. Stage 5 schema rejects with E_SCHEMA_DUPLICATE_ENTRY." + ), + spec_refs=["§06", "§07", "§11"], + verdict="reject", + diagnostic="E_SCHEMA_DUPLICATE_ENTRY", + body_obj=m_dup_policy, + context={"fetched_origin_address": m_dup_policy["origin"]["address"]}, + )) + + # ---- 138-schema-malformed-unicode (Stage 5, E_SCHEMA_MALFORMED_UNICODE) ---- + # Manifest whose canary.statement contains the JSON escape sequence + # \uD800 - a lone high surrogate with no paired low surrogate. RFC 8259 + # admits the escape syntactically; §04 rejects the resulting isolated + # surrogate code point at schema validation. Raw bytes are used so the + # surrogate appears literally in the wire form (Python's UTF-8 encoder + # would otherwise refuse to emit it). + out.append(vec( + "138-schema-malformed-unicode", + kind="manifest", + description=( + "Manifest whose canary.statement contains the JSON escape " + "sequence \\uD800 - a lone high surrogate with no paired low " + "surrogate. After JSON parsing this yields a string with an " + "isolated surrogate code point, which §04 rejects as malformed " + "Unicode at Stage 5 schema validation (before Stage 6 signature " + "verification). Conforming parsers accept the JSON escape per " + "RFC 8259 §7; rejection is at the schema layer. " + "E_SCHEMA_MALFORMED_UNICODE." + ), + spec_refs=["§04", "§11"], + verdict="reject", + diagnostic="E_SCHEMA_MALFORMED_UNICODE", + body=( + b'{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"' + + b64u(pp_pub).encode("ascii") + + b'","origin":{"carrier":"tor-v3","address":"' + + onion_address(op_pub).encode("ascii") + + b'","origin_pubkey":"' + + b64u(op_pub).encode("ascii") + + b'"},"canary":{"runtime_pubkey":"' + + b64u(rp_pub).encode("ascii") + + b'","issued_at":"2026-05-07T00:00:00Z",' + b'"next_expected":"2026-06-06T00:00:00Z",' + b'"statement":"Lone surrogate: \\uD800."},' + b'"state_policy":[],"navigation":[],' + b'"min_refresh_interval":3600,' + b'"updated":"2026-05-07T00:00:00Z","sig":"' + + (b"A" * 86) + b'"}' + ), + )) + + # ---- 175-bind-origin (Stage 9, E_BIND_ORIGIN) ---- + # Otherwise-valid manifest binding origin to K_origin (op_pub), but + # fetched from the address derived from K_origin_2 (op_pub_2). Stage 9 + # Tor v3 address-to-key derivation fires the binding error. + m_175 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + ) + wrong_origin_address = onion_address(keys["origin_pub_2"]) + out.append(vec( + "175-bind-origin", + kind="manifest", + description=( + "Otherwise-valid manifest whose origin binds to K_origin (test " + "fixture), fetched from the onion address derived from K_origin_2. " + "Stage 9 Tor v3 address-to-key derivation produces an " + "origin_pubkey that does not match manifest.origin.origin_pubkey. " + "E_BIND_ORIGIN." + ), + spec_refs=["§05", "§09", "§10"], + verdict="reject", + diagnostic="E_BIND_ORIGIN", + body_obj=m_175, + context={"fetched_origin_address": wrong_origin_address}, + )) + + # ---- 176-origin-invalid (E_ORIGIN_INVALID) ---- + # Manifest whose origin.not_after equals canary.issued_at. §06 requires + # not_after to be strictly later than canary.issued_at; equal violates + # the MUST. + m_176 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + not_after="2026-05-07T00:00:00Z", # equal to canary.issued_at + ) + out.append(vec( + "176-origin-invalid", + kind="manifest", + description=( + "Manifest whose origin.not_after equals canary.issued_at " + "(2026-05-07T00:00:00Z). §06 requires not_after strictly later " + "than canary.issued_at; equality violates the MUST. The " + "manifest is signed correctly and otherwise valid. " + "E_ORIGIN_INVALID." + ), + spec_refs=["§06", "§11"], + verdict="reject", + diagnostic="E_ORIGIN_INVALID", + body_obj=m_176, + context={"fetched_origin_address": m_176["origin"]["address"]}, + )) + + # ---- 182-canary-invalid (Stage 8, E_CANARY_INVALID) ---- + # Canary interval (next_expected - issued_at) is 6 days, below the + # 7-day minimum required by §08. All other fields are valid and the + # manifest is signed correctly. Canary is fresh at clock_now. + m_182 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + issued_at="2026-05-06T00:00:00Z", + next_expected="2026-05-12T00:00:00Z", # 6 days, below 7-day floor + updated="2026-05-06T00:00:00Z", + ) + out.append(vec( + "182-canary-invalid", + kind="manifest", + description=( + "Manifest whose canary interval (next_expected - issued_at) is " + "6 days, below the 7-day minimum required by §08. The manifest " + "is otherwise well-formed and signed correctly; canary is fresh " + "at clock_now (2026-05-07). Stage 8 canary validation fires " + "E_CANARY_INVALID." + ), + spec_refs=["§08", "§11"], + verdict="reject", + diagnostic="E_CANARY_INVALID", + body_obj=m_182, + context={"fetched_origin_address": m_182["origin"]["address"]}, + )) + + # ===================================================================== + # rc.19 Lotto 16 corpus additions: vectors filling diagnostic codes + # that remained zero-covered after rc.18 Phase-1 but are reachable + # within the existing single-document or already-supported + # multi-manifest schema. Each vector observes the corpus isolation + # rule (only the targeted diagnostic-relevant violation is live at + # the first failing pipeline stage). + # ===================================================================== + + # ---- 139-schema-field-length (Stage 5, E_SCHEMA_FIELD_LENGTH) ---- + # Manifest whose canary.freshness_proof is a 201-byte ASCII string, + # one byte above the 200-byte cap declared in §08:118. The string is + # well within the Stage 3 100 KiB parser cap (§04) so Stage 3 passes; + # Stage 5 schema validation fires the field-specific length cap as + # distinct from the parser-level cap that E_PARSE_STRING_LENGTH + # covers. ASCII is NFC and contains no control characters so the only + # live violation at the first failing stage is the length cap on + # freshness_proof (corpus isolation rule). statement is left at its + # normal short valid value from make_manifest; freshness_proof is + # optional, this vector explicitly adds it to exercise the cap. + fp_201 = "x" * 201 + m_139 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + ) + m_139["canary"]["freshness_proof"] = fp_201 + m_139["sig"] = sign(pp, CTX_MANIFEST, m_139) + out.append(vec( + "139-schema-field-length", + kind="manifest", + description=( + "Manifest whose canary.freshness_proof is a 201-byte ASCII " + "string, one byte above the 200-byte cap declared in " + "§08:118. The string is well within the Stage 3 100 KiB " + "parser cap (§04) so Stage 3 passes; Stage 5 schema " + "validation fires E_SCHEMA_FIELD_LENGTH for the " + "field-specific cap, distinct from the parser-level " + "E_PARSE_STRING_LENGTH (vector 112)." + ), + spec_refs=["§08", "§11"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_LENGTH", + body_obj=m_139, + context={"fetched_origin_address": m_139["origin"]["address"]}, + )) + + # ---- 156-sig-invalid-key-no-manifest (Stage 6, E_SIG_INVALID_KEY) ---- + # Content document presented without any previously verified manifest + # for its publisher. Per §11:175, the absence of an authorized + # runtime_pubkey to verify against is E_SIG_INVALID_KEY, distinct + # from E_SIG_VERIFICATION (signature decoded and the verify equation + # failed). The content body and signature are themselves well-formed; + # the failure is the missing key context. Vector context deliberately + # omits `expected_runtime_pubkey` and `previously_verified` to model + # the no-manifest condition. + c_156 = make_content(runtime_priv=rp, path="/articles/orphan-content") + out.append(vec( + "156-sig-invalid-key-no-manifest", + kind="content", + description=( + "Content document presented without any verified manifest " + "supplying an authorized runtime_pubkey for the publisher. " + "Per §11:172,175 the absence of the expected verification " + "key yields E_SIG_INVALID_KEY, distinct from " + "E_SIG_VERIFICATION which requires a key that decodes and " + "fails the verify equation. The content body and signature " + "are themselves well-formed; the failure is the missing key " + "context. Vector context deliberately omits " + "expected_runtime_pubkey and previously_verified." + ), + spec_refs=["§05", "§11"], + verdict="reject", + diagnostic="E_SIG_INVALID_KEY", + body_obj=c_156, + context={"fetched_path": c_156["path"]}, + )) + + # ---- 157-sig-small-order-r (Stage 6, E_SIG_VERIFICATION) ---- + # Manifest whose signature R component is the encoded identity point + # (small-order, order 1), the same encoding pattern as + # SMALL_ORDER_A used by vector 153 for the public key. Per §05:174 + # (rc.22 N63), the strict profile rejects small-order R alongside + # small-order A; the rejection symmetry between A and R was + # explicitly mandated in N63 to align the spec text with what + # ed25519-dalek's verify_strict actually does (it rejects R via + # signature_R.is_small_order() before the verify equation). + # Pair to vector 153 for A; both fire E_SIG_VERIFICATION. + # The S half of the signature is left at the value from the + # original valid signature on m; the small-order R rejection takes + # precedence over any subsequent verification step. + real_sig_bytes_157 = b64u_decode(m["sig"]) + small_r_sig = SMALL_ORDER_A + real_sig_bytes_157[32:] + m_157 = dict(m) + m_157["sig"] = b64u(small_r_sig) + out.append(vec( + "157-sig-small-order-r", + kind="manifest", + description=( + "Manifest whose signature R component is the encoded " + "identity point (small-order, order 1), the same encoding " + "pattern as vector 153 uses for the public key A. Per " + "§05:174 (rc.22 N63), the strict profile rejects small-order " + "R alongside small-order A, matching the verify_strict mode " + "in ed25519-dalek (src/verifying.rs) which calls " + "signature_R.is_small_order() before the verify equation. " + "Pair to vector 153 for A; both fire E_SIG_VERIFICATION." + ), + spec_refs=["§05", "§11"], + verdict="reject", + diagnostic="E_SIG_VERIFICATION", + body_obj=m_157, + context={"fetched_origin_address": m_157["origin"]["address"]}, + )) + + # ---- 177-origin-invalid-beyond-5y (E_ORIGIN_INVALID, second reason) ---- + # Manifest whose origin.not_after is more than 5 years after + # canary.issued_at. §06 caps not_after at 5 years past issued_at; this + # vector pairs with 176 (the not_after_not_later_than_issued_at reason) + # to cover both reason values declared in §11 E_ORIGIN_INVALID + # structured details. + m_177 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + not_after="2031-05-08T00:00:00Z", # >5y after issued_at 2026-05-07 + ) + out.append(vec( + "177-origin-invalid-beyond-5y", + kind="manifest", + description=( + "Manifest whose origin.not_after is 2031-05-08, more than 5 " + "years after canary.issued_at (2026-05-07). §06 forbids " + "not_after beyond 5 years past issued_at. This vector pairs " + "with 176 (equal-to-issued_at reason) to cover both reason " + "values of E_ORIGIN_INVALID structured details " + "(not_after_beyond_5y vs not_after_not_later_than_issued_at)." + ), + spec_refs=["§06", "§11"], + verdict="reject", + diagnostic="E_ORIGIN_INVALID", + diagnostic_details={"reason": "not_after_beyond_5y"}, + body_obj=m_177, + context={"fetched_origin_address": m_177["origin"]["address"]}, + )) + + # ---- 178-manifest-updated-future-skew (Stage 5, E_SCHEMA_FIELD_SYNTAX) ---- + # Manifest whose `updated` is set to 2026-05-07T00:07:00Z, six minutes + # ahead of clock_now (2026-05-07T00:01:00Z), exceeding the 300-second + # future-skew tolerance defined in §10. Per §06:342 and §10:815, this + # is rejected as E_SCHEMA_FIELD_SYNTAX with structured details + # reason="future_beyond_skew_tolerance". Distinct from canary + # issued_at future skew (vector 183, which yields E_CANARY_INVALID). + m_178 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + updated="2026-05-07T00:07:00Z", + ) + out.append(vec( + "178-manifest-updated-future-skew", + kind="manifest", + description=( + "Manifest whose `updated` is set 6 minutes ahead of clock_now " + "(2026-05-07T00:07:00Z vs 2026-05-07T00:01:00Z), exceeding " + "the 300-second future-skew tolerance defined in §10. Per " + "§06:342 and §10:815, this is rejected as " + "E_SCHEMA_FIELD_SYNTAX with structured details " + "reason=future_beyond_skew_tolerance. The manifest is signed " + "correctly and otherwise valid; the temporal-domain failure " + "is the only live violation at Stage 5." + ), + spec_refs=["§06", "§10", "§11"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_SYNTAX", + diagnostic_details={"reason": "future_beyond_skew_tolerance"}, + body_obj=m_178, + context={"fetched_origin_address": m_178["origin"]["address"]}, + )) + + # ---- 183-canary-issued-at-future-skew (Stage 8, E_CANARY_INVALID) ---- + # Manifest whose canary.issued_at is 6 minutes ahead of clock_now, + # exceeding the 300-second future-skew tolerance defined in §10. Per + # §08:68,156 this is one of the named E_CANARY_INVALID conditions + # (issued_at implausibly in the future). The manifest signature is + # valid and the canary interval falls within the 7-to-30-day bounds; + # the only live violation at Stage 8 is the temporal-skew check. + m_183 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + issued_at="2026-05-07T00:07:00Z", + next_expected="2026-06-06T00:07:00Z", + updated="2026-05-07T00:00:00Z", + ) + out.append(vec( + "183-canary-issued-at-future-skew", + kind="manifest", + description=( + "Manifest whose canary.issued_at is 2026-05-07T00:07:00Z, 6 " + "minutes ahead of clock_now (2026-05-07T00:01:00Z), " + "exceeding the 300-second future-skew tolerance defined in " + "§10. Per §08:68,156 this is one of the named " + "E_CANARY_INVALID conditions. The manifest signature is " + "valid, the canary interval is within bounds, and `updated` " + "is kept at clock_now-1 to avoid competing with the §06 " + "future-skew check exercised by vector 178: the Stage 8 " + "issued_at check is the only live skew violation." + ), + spec_refs=["§08", "§10", "§11"], + verdict="reject", + diagnostic="E_CANARY_INVALID", + body_obj=m_183, + context={"fetched_origin_address": m_183["origin"]["address"]}, + )) + + # ---- 184-canary-runtime-reuse (Stage 8, E_CANARY_RUNTIME_REUSE) ---- + # Multi-manifest scenario: the previously verified manifest is dated + # 2026-04-30 (carried in extra_files to avoid coupling to the 001 + # positive fixture and keep the live manifest at clock_now without + # creating a future-skew confound). The presented manifest at + # clock_now (issued_at 2026-05-07) declares the same runtime_pubkey + # as the prior. Per §08 (rc.19 N55) and §11:200, rotation MUST + # produce a distinct runtime_pubkey; reuse is rejected as + # E_CANARY_RUNTIME_REUSE at Stage 8. Both the prior and the + # presented manifest are signed correctly, are within canary + # interval bounds, and have updated <= clock_now+300s so the only + # live Stage 8 violation is the rotation-proof failure. + m_184_prior = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + issued_at="2026-04-30T00:00:00Z", + next_expected="2026-05-30T00:00:00Z", + updated="2026-04-30T00:00:00Z", + ) + m_184_prior_bytes = json.dumps( + m_184_prior, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + m_184 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + # default issued_at=2026-05-07, updated=2026-05-07; same runtime_pub + ) + out.append(vec( + "184-canary-runtime-reuse", + kind="manifest", + description=( + "Multi-manifest scenario: a previously verified manifest " + "dated 2026-04-30 (carried in extra_files as " + "prior_manifest.json) authorizes runtime_pubkey X. The " + "presented manifest at clock_now (issued_at 2026-05-07) for " + "the same K_publisher.pub declares the same runtime_pubkey " + "X. Per §08 (rc.19 N55) and §11:200, rotation MUST produce " + "a distinct runtime_pubkey; reuse is rejected as " + "E_CANARY_RUNTIME_REUSE at Stage 8. Both manifests are " + "signed correctly and otherwise valid; the rotation-proof " + "failure is the only live Stage 8 violation." + ), + spec_refs=["§08", "§11"], + verdict="reject", + diagnostic="E_CANARY_RUNTIME_REUSE", + diagnostic_details={ + "runtime_pubkey": b64u(rp_pub), + "previous_issued_at": "2026-04-30T00:00:00Z", + "current_issued_at": "2026-05-07T00:00:00Z", + "window_position": 1, + }, + body_obj=m_184, + context={ + "fetched_origin_address": m_184["origin"]["address"], + "previously_verified": "vectors/184-canary-runtime-reuse/prior_manifest.json", + }, + extra_files={ + "prior_manifest.json": m_184_prior_bytes, + }, + )) + + # ---- 185-canary-runtime-reuse-resurrection (Stage 8, + # E_CANARY_RUNTIME_REUSE with window_position >= 2) ---- + # + # Three-manifest A -> B -> A resurrection scenario. The publisher + # history (carried in extra_files as prior_manifest_a.json and + # prior_manifest_b.json) contains: + # M_A at issued_at 2026-04-23 with runtime_pubkey X (= rp_pub) + # M_B at issued_at 2026-04-30 with runtime_pubkey Y (= runtime_pub_2) + # The presented manifest is M_C at issued_at 2026-05-07 with + # runtime_pubkey X again (the rp_pub from M_A; not the immediately + # preceding M_B's Y). Per §08 immediate-preceding MUST: M_C is + # accepted at the MUST level because X != Y. Per §08 SHOULD for + # clients maintaining runtime-pubkey history: M_C is rejected + # because X is present in publisher history (M_A entry), with + # E_CANARY_RUNTIME_REUSE.details.window_position = 2 (the match is + # two entries back: M_A is two positions before M_C in the + # ordered history M_A -> M_B -> M_C, i.e. M_A is the entry before + # the immediately preceding M_B). Stateless clients accept M_C + # (per §00 N60 limitation); stateful clients reject. This is the + # canonical demonstration vector for the rc.19 N60 SHOULD. + # + # The corpus verdict records the stateful-client rejection + # because that is the SHOULD path; stateless clients diverge by + # design and a conformant stateless implementation reporting + # accept on this vector is operating within the §00 N60 + # limitation. The vector's extended context field + # `previously_verified_history` is a sequence of prior manifest + # paths in publication order (oldest first) used by stateful + # clients to populate their runtime-pubkey history before + # presenting the vector input. + m_185_a = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + issued_at="2026-04-23T00:00:00Z", + next_expected="2026-05-23T00:00:00Z", + updated="2026-04-23T00:00:00Z", + ) + m_185_a_bytes = json.dumps( + m_185_a, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + m_185_b = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=keys["runtime_pub_2"], + issued_at="2026-04-30T00:00:00Z", + next_expected="2026-05-30T00:00:00Z", + updated="2026-04-30T00:00:00Z", + ) + m_185_b_bytes = json.dumps( + m_185_b, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + # Presented manifest M_C: resurrects runtime_pubkey X from M_A. + # Default issued_at=2026-05-07 is strictly newer than M_B + # 2026-04-30 so the immediate-preceding MUST passes (X != Y at + # window_position=1); the SHOULD fires at window_position=2. + m_185_c = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + ) + out.append(vec( + "185-canary-runtime-reuse-resurrection", + kind="manifest", + description=( + "A -> B -> A resurrection scenario. The publisher history " + "(extra_files) contains M_A (issued_at 2026-04-23, " + "runtime_pubkey X) and M_B (issued_at 2026-04-30, " + "runtime_pubkey Y). The presented manifest M_C at " + "clock_now (issued_at 2026-05-07) declares runtime_pubkey " + "X again, resurrecting the M_A key after M_B retired it. " + "Per §08 immediate-preceding MUST, M_C passes (X != Y). " + "Per §08 SHOULD for clients maintaining runtime-pubkey " + "history, M_C is rejected as E_CANARY_RUNTIME_REUSE with " + "details.window_position = 2 (M_A is two entries back). " + "Stateless clients accept (per §00 N60 limitation); the " + "corpus verdict records the stateful-client rejection." + ), + spec_refs=["§08", "§00", "§11"], + verdict="reject", + diagnostic="E_CANARY_RUNTIME_REUSE", + diagnostic_details={ + "runtime_pubkey": b64u(rp_pub), + "previous_issued_at": "2026-04-23T00:00:00Z", + "current_issued_at": "2026-05-07T00:00:00Z", + "window_position": 2, + }, + body_obj=m_185_c, + context={ + "fetched_origin_address": m_185_c["origin"]["address"], + "previously_verified_history": [ + "vectors/185-canary-runtime-reuse-resurrection/prior_manifest_a.json", + "vectors/185-canary-runtime-reuse-resurrection/prior_manifest_b.json", + ], + }, + extra_files={ + "prior_manifest_a.json": m_185_a_bytes, + "prior_manifest_b.json": m_185_b_bytes, + }, + )) + + # ---- 191-unicode-nfd-freshness-proof (Stage 5, E_SCHEMA_FIELD_SYNTAX) ---- + # Parity with vector 190 (statement NFD): manifest whose + # canary.freshness_proof contains a decomposed combining mark + # (NFD) rather than the precomposed NFC form. §04 plus the §08 + # explicit MUST NFC for freshness_proof (rc.19 N59) require the + # field to be NFC. Rejected at schema validation with + # E_SCHEMA_FIELD_SYNTAX before signature verification. + # freshness_proof "Cafe(acute) block-871234" in NFD: "Cafe" + U+0301 + " block-871234" + nfd_freshness = "Cafe\u0301 block-871234" + m_191_payload = { + "spec_version": "1.0", + "kind": "manifest", + "publisher_pubkey": b64u(pp_pub), + "origin": { + "carrier": "tor-v3", + "address": onion_address(op_pub), + "origin_pubkey": b64u(op_pub), + }, + "canary": { + "runtime_pubkey": b64u(rp_pub), + "issued_at": "2026-05-07T00:00:00Z", + "next_expected": "2026-06-06T00:00:00Z", + "statement": "No warrants received.", + "freshness_proof": nfd_freshness, + }, + "state_policy": [], + "navigation": [], + "min_refresh_interval": 3600, + "updated": "2026-05-07T00:00:00Z", + } + m_191_payload["sig"] = sign(pp, CTX_MANIFEST, m_191_payload) + out.append(vec( + "191-unicode-nfd-freshness-proof", + kind="manifest", + description="Manifest whose canary.freshness_proof contains a decomposed combining mark (NFD) rather than the precomposed NFC form. Per §04 and the §08 explicit NFC rule for freshness_proof (rc.19 N59), user-visible strings must be in NFC. Rejected at schema validation with E_SCHEMA_FIELD_SYNTAX before signature verification. Parity with vector 190 for canary.statement.", + spec_refs=["§04", "§08"], + verdict="reject", + diagnostic="E_SCHEMA_FIELD_SYNTAX", + body_obj=m_191_payload, + context={"fetched_origin_address": m_191_payload["origin"]["address"]}, + )) + + # ---- 201-migration-chain-cycle (E_MIGRATION_INVALID chain_cycle) ---- + # + # Two-manifest scenario realizing the deterministic A -> B -> A chain + # cycle. The announcing manifest at origin A (op_pub) carries a + # migration_pointer to successor B (op_pub_2). The successor manifest + # at B is signed correctly and binds correctly, but its own + # migration_pointer announces a return to A (op_pub). Per §10:436, + # the visited_origins set populated during a single migration + # resolution flow forbids re-adopting an address already in the set; + # B's announcement of A is therefore rejected as E_MIGRATION_INVALID + # with details.reason="chain_cycle". The diagnostic is deterministic + # across conforming clients: any client tracking visited_origins per + # §10 will reject on the second hop regardless of chain-depth policy. + # The vector pairs the announcing manifest at A as the primary input; + # the successor manifest at B is provided in extra_files. The verdict + # refers to the migration adoption outcome, not the in-isolation + # validity of the announcing manifest. + op_pub_2_201 = keys["origin_pub_2"] + address_a = onion_address(op_pub) + address_b = onion_address(op_pub_2_201) + successor_b = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub_2_201, runtime_pub=rp_pub, + migration_pointer={ + "successor_origin": { + "carrier": "tor-v3", + "address": address_a, # back to A + "origin_pubkey": b64u(op_pub), + }, + "announced_at": "2026-05-07T00:00:00Z", + }, + ) + successor_b_bytes = json.dumps( + successor_b, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + announcing_a = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + migration_pointer={ + "successor_origin": { + "carrier": "tor-v3", + "address": address_b, + "origin_pubkey": b64u(op_pub_2_201), + }, + "announced_at": "2026-05-07T00:00:00Z", + }, + ) + out.append(vec( + "201-migration-chain-cycle", + kind="manifest", + description=( + "Two-manifest chain-cycle scenario A -> B -> A. The " + "announcing manifest at origin A carries a migration_pointer " + "to successor B (op_pub_2). The successor at B is signed " + "correctly and binds correctly, but its own " + "migration_pointer announces a return to A. Per §10:436, " + "the per-flow visited_origins set forbids re-adopting an " + "address already visited; B's announcement of A is rejected " + "as E_MIGRATION_INVALID with details.reason='chain_cycle'. " + "The diagnostic is deterministic across conforming clients " + "(any client tracking visited_origins per §10 rejects on " + "the second hop, regardless of chain-depth policy)." + ), + spec_refs=["§06", "§10", "§11"], + verdict="reject", + diagnostic="E_MIGRATION_INVALID", + diagnostic_details={ + "reason": "chain_cycle", + "announcing_origin_address": address_b, + "successor_origin_address": address_a, + }, + body_obj=announcing_a, + context={ + "fetched_origin_address": address_a, + "successor_origin_address": address_b, + "successor_manifest_path": "vectors/201-migration-chain-cycle/successor_manifest.json", + }, + extra_files={ + "successor_manifest.json": successor_b_bytes, + }, + )) + + # ---- 202-migration-successor-key-mismatch (E_MIGRATION_INVALID + # successor_key_mismatch) ---- + # + # Single-manifest, announcement-internal check (§06). The announcing + # manifest at origin A (op_pub) carries a migration_pointer whose + # successor_origin.address is the onion address of op_pub_2 but whose + # declared origin_pubkey is op_pub. For Tor v3 the address decodes to a + # public key (op_pub_2) that does not equal the declared origin_pubkey + # (op_pub), violating the §06 address-to-key binding for the successor + # pointer. This is evaluated when the announcing manifest is validated; + # it does not require fetching the successor (distinct from the §10 + # fetch-time E_MIGRATION_MISMATCH checks). Rejected as + # E_MIGRATION_INVALID with details.reason="successor_key_mismatch". + address_a_202 = onion_address(op_pub) + address_b_202 = onion_address(op_pub_2) + announcing_202 = make_manifest( + publisher_priv=pp, publisher_pub=pp_pub, + origin_pub=op_pub, runtime_pub=rp_pub, + migration_pointer={ + "successor_origin": { + "carrier": "tor-v3", + "address": address_b_202, # decodes to op_pub_2 + "origin_pubkey": b64u(op_pub), # but declares op_pub + }, + "announced_at": "2026-05-07T00:00:00Z", + }, + ) + out.append(vec( + "202-migration-successor-key-mismatch", + kind="manifest", + description=( + "Announcement-internal successor binding failure (§06). The " + "migration_pointer.successor_origin.address decodes to op_pub_2 " + "but the declared successor_origin.origin_pubkey is op_pub, so " + "the address does not decode to the declared key. Per §06 the " + "client MUST verify this binding before treating the " + "announcement as valid; failure is E_MIGRATION_INVALID with " + "details.reason='successor_key_mismatch'. The check is " + "evaluated on the announcing manifest alone and does not fetch " + "the successor (distinct from the §10 fetch-time " + "E_MIGRATION_MISMATCH path)." + ), + spec_refs=["§05", "§06", "§11"], + verdict="reject", + diagnostic="E_MIGRATION_INVALID", + diagnostic_details={ + "reason": "successor_key_mismatch", + "announcing_origin_address": address_a_202, + "successor_origin_address": address_b_202, + }, + body_obj=announcing_202, + context={ + "fetched_origin_address": address_a_202, + }, + )) + + return out + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +def main() -> int: + # Reset the vectors directory so generation is fresh and reproducible. + if VECTORS_DIR.exists(): + shutil.rmtree(VECTORS_DIR) + VECTORS_DIR.mkdir(parents=True) + + publisher_priv, publisher_pub = keypair(PUBLISHER_SEED) + runtime_priv, runtime_pub = keypair(RUNTIME_SEED) + origin_priv, origin_pub = keypair(ORIGIN_SEED) + runtime_priv_2, runtime_pub_2 = keypair(RUNTIME_SEED_2) + origin_priv_2, origin_pub_2 = keypair(ORIGIN_SEED_2) + + keys = { + "publisher_priv": publisher_priv, + "publisher_pub": publisher_pub, + "runtime_priv": runtime_priv, + "runtime_pub": runtime_pub, + "origin_priv": origin_priv, + "origin_pub": origin_pub, + "runtime_priv_2": runtime_priv_2, + "runtime_pub_2": runtime_pub_2, + "origin_priv_2": origin_priv_2, + "origin_pub_2": origin_pub_2, + } + + wordlist = load_bip39_wordlist() + publisher_entry = { + "seed_hex": PUBLISHER_SEED.hex(), + "pub_b64u": b64u(publisher_pub), + } + if wordlist is not None: + publisher_entry["pip"] = compute_pip(publisher_pub, wordlist) + + keys_doc = { + "_comment": "Test fixtures only. NEVER use these for any real deployment.", + "publisher": publisher_entry, + "runtime": { + "seed_hex": RUNTIME_SEED.hex(), + "pub_b64u": b64u(runtime_pub), + }, + "origin": { + "seed_hex": ORIGIN_SEED.hex(), + "pub_b64u": b64u(origin_pub), + "tor_v3_address": onion_address(origin_pub), + }, + "runtime_2": { + "seed_hex": RUNTIME_SEED_2.hex(), + "pub_b64u": b64u(runtime_pub_2), + }, + "origin_2": { + "seed_hex": ORIGIN_SEED_2.hex(), + "pub_b64u": b64u(origin_pub_2), + "tor_v3_address": onion_address(origin_pub_2), + }, + } + (ROOT / "keys.json").write_bytes( + (json.dumps(keys_doc, indent=2, ensure_ascii=False) + "\n") + .encode("utf-8") + ) + + vectors: list[dict] = [] + vectors.extend(positive_vectors(keys)) + vectors.extend(negative_vectors(keys)) + + corpus = { + "_comment": "Generated by corpus/tools/generate.py. Do not hand-edit.", + "spec_version_target": "1.0", + "rc_target": "1.0-rc.27", + "keys": "keys.json", + "clock_now": "2026-05-07T00:01:00Z", + "vectors": vectors, + } + (ROOT / "corpus.json").write_bytes( + (json.dumps(corpus, indent=2, ensure_ascii=False) + "\n") + .encode("utf-8") + ) + + print(f"Generated {len(vectors)} vectors -> {VECTORS_DIR}") + print(f" positive: {sum(1 for v in vectors if v['expected']['verdict'] == 'accept')}") + print(f" negative: {sum(1 for v in vectors if v['expected']['verdict'] == 'reject')}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/test/resources/corpus/vectors/001-manifest-valid-minimal/input.json b/src/test/resources/corpus/vectors/001-manifest-valid-minimal/input.json new file mode 100644 index 0000000..15ae176 --- /dev/null +++ b/src/test/resources/corpus/vectors/001-manifest-valid-minimal/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/002-manifest-valid-state-policy/input.json b/src/test/resources/corpus/vectors/002-manifest-valid-state-policy/input.json new file mode 100644 index 0000000..5ee0c3f --- /dev/null +++ b/src/test/resources/corpus/vectors/002-manifest-valid-state-policy/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[{"namespace":"session","key":"auth","mode":"request","max_size":512,"max_lifetime":86400,"purpose":"Authenticate submit requests after login."},{"namespace":"ui","key":"lang","mode":"client_only","max_size":32,"max_lifetime":7776000,"purpose":"Remember the chosen language for the user interface."}],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"IMSZkTl8Ehp273s7TZ1kbDMKH7IFL9pQPW_azF4fKGRZq7G02_hYWnyUsy0tXSXGU8wibnEGvMENSWXNBcoKAw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/003-content-valid-minimal/input.json b/src/test/resources/corpus/vectors/003-content-valid-minimal/input.json new file mode 100644 index 0000000..978b740 --- /dev/null +++ b/src/test/resources/corpus/vectors/003-content-valid-minimal/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/articles/first-post","meta":{"title":"First post","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"paragraph","content":[{"kind":"text","value":"Hello, world.","marks":[]}]}],"sig":"1RNUD18vC4f2LTyOYj5BeVHBrLaH_G_wgYPyR5ux7yll2_mngS8mQV23JZTs2WtBecTgy-w-lCm6uWJDhjuuAQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/004-content-valid-blocks-showcase/input.json b/src/test/resources/corpus/vectors/004-content-valid-blocks-showcase/input.json new file mode 100644 index 0000000..604802c --- /dev/null +++ b/src/test/resources/corpus/vectors/004-content-valid-blocks-showcase/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/articles/blocks-showcase","meta":{"title":"Block showcase","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"heading","level":1,"content":[{"kind":"text","value":"Showcase","marks":[]}]},{"kind":"paragraph","content":[{"kind":"text","value":"An example of ","marks":[]},{"kind":"text","value":"bold","marks":["bold"]},{"kind":"text","value":" and ","marks":[]},{"kind":"text","value":"italic","marks":["italic"]},{"kind":"text","value":" text.","marks":[]}]},{"kind":"list","ordered":false,"items":[[{"kind":"text","value":"First","marks":[]}],[{"kind":"text","value":"Second","marks":[]}]]},{"kind":"code_block","language":"rust","content":"fn main() {\n println!(\"hi\");\n}"},{"kind":"divider"},{"kind":"quote","content":[{"kind":"text","value":"Lorem ipsum.","marks":[]}]}],"sig":"Eh8Y0MiYS4EXfyI5GaMvpHNRPUNFFHOLN_qeHIsalhoptmmr_YNyQBd1Mpl9A1Koku_UBKWpndjA06CLQWqIBw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/005-transaction-valid-minimal/input.json b/src/test/resources/corpus/vectors/005-transaction-valid-minimal/input.json new file mode 100644 index 0000000..0242176 --- /dev/null +++ b/src/test/resources/corpus/vectors/005-transaction-valid-minimal/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"transaction","in_response_to":"/contact","request_id":"AAECAwQFBgcICQoLDA0ODw","request_hash":"sha-256:-EvECkoil9nNYYBfRQE85W5pWojAP0K9UG830mtQn0M","state_updates":[],"blocks":[{"kind":"feedback","variant":"success","content":[{"kind":"text","value":"Received.","marks":[]}]}],"sig":"n8b1lkrg62RQB8ZGDsJbvU_U3m5qQXaVyZPYasnQdG9VhT9Cyvw0JOFZaQMGzhNkCq0rt-Apmpezxpvj7DIBDw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/005-transaction-valid-minimal/submit_body.json b/src/test/resources/corpus/vectors/005-transaction-valid-minimal/submit_body.json new file mode 100644 index 0000000..a577fb4 --- /dev/null +++ b/src/test/resources/corpus/vectors/005-transaction-valid-minimal/submit_body.json @@ -0,0 +1 @@ +{"fields":{"message":"hello","name":"alice"},"request_state":[],"request_id":"AAECAwQFBgcICQoLDA0ODw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/006-manifest-valid-not-after/input.json b/src/test/resources/corpus/vectors/006-manifest-valid-not-after/input.json new file mode 100644 index 0000000..363f764 --- /dev/null +++ b/src/test/resources/corpus/vectors/006-manifest-valid-not-after/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo","not_after":"2027-05-07T00:00:00Z"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"UmfK2viNmvMj99Kt6nhM_fXBISXw_KBLyZbGpjGwMpofhDUu3rlubDulUi3tGcEki1GteI0jFvUIbLLedAXPAw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/007-content-valid-large-seq/input.json b/src/test/resources/corpus/vectors/007-content-valid-large-seq/input.json new file mode 100644 index 0000000..4f48de4 --- /dev/null +++ b/src/test/resources/corpus/vectors/007-content-valid-large-seq/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/articles/large-seq","seq":9007199254740993,"meta":{"title":"Large seq","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"paragraph","content":[{"kind":"text","value":"Hello, world.","marks":[]}]}],"sig":"w1oz6XvFrt3G75AYVmUDF2oOgadJ7lgpBKnJXXoFCcOcgELvVkfU42neHl70y_V-2-5N8PHcoRMPcZlQuf96Aw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/100-input-bom/input.json b/src/test/resources/corpus/vectors/100-input-bom/input.json new file mode 100644 index 0000000..6ae28c4 --- /dev/null +++ b/src/test/resources/corpus/vectors/100-input-bom/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/101-input-bad-utf8/input.json b/src/test/resources/corpus/vectors/101-input-bad-utf8/input.json new file mode 100644 index 0000000..eef44e2 --- /dev/null +++ b/src/test/resources/corpus/vectors/101-input-bad-utf8/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","x":""} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/102-input-byte-cap/input.json b/src/test/resources/corpus/vectors/102-input-byte-cap/input.json new file mode 100644 index 0000000..d503c6e --- /dev/null +++ b/src/test/resources/corpus/vectors/102-input-byte-cap/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","_pad":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/110-parse-duplicate-keys/input.json b/src/test/resources/corpus/vectors/110-parse-duplicate-keys/input.json new file mode 100644 index 0000000..744645d --- /dev/null +++ b/src/test/resources/corpus/vectors/110-parse-duplicate-keys/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/x","path":"/y","meta":{"title":"t","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"divider"}],"sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/111-parse-nesting-depth/input.json b/src/test/resources/corpus/vectors/111-parse-nesting-depth/input.json new file mode 100644 index 0000000..638bda2 --- /dev/null +++ b/src/test/resources/corpus/vectors/111-parse-nesting-depth/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","_nest":[[[[[[[[[[[[[[[[[[[[0]]]]]]]]]]]]]]]]]]]],"sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/112-parse-string-length/input.json b/src/test/resources/corpus/vectors/112-parse-string-length/input.json new file mode 100644 index 0000000..004af10 --- /dev/null +++ b/src/test/resources/corpus/vectors/112-parse-string-length/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/x","meta":{"title":"t","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"code_block","language":"text","content":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}],"sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/113-parse-array-length/input.json b/src/test/resources/corpus/vectors/113-parse-array-length/input.json new file mode 100644 index 0000000..3faee47 --- /dev/null +++ b/src/test/resources/corpus/vectors/113-parse-array-length/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/x","meta":{"title":"t","published_at":"2026-05-07T00:00:00Z"},"blocks":[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}],"sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/114-parse-object-keys/input.json b/src/test/resources/corpus/vectors/114-parse-object-keys/input.json new file mode 100644 index 0000000..049d4ad --- /dev/null +++ b/src/test/resources/corpus/vectors/114-parse-object-keys/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/x","meta":{"title":"t","published_at":"2026-05-07T00:00:00Z","k0":0,"k1":0,"k2":0,"k3":0,"k4":0,"k5":0,"k6":0,"k7":0,"k8":0,"k9":0,"k10":0,"k11":0,"k12":0,"k13":0,"k14":0,"k15":0,"k16":0,"k17":0,"k18":0,"k19":0,"k20":0,"k21":0,"k22":0,"k23":0,"k24":0,"k25":0,"k26":0,"k27":0,"k28":0,"k29":0,"k30":0,"k31":0,"k32":0,"k33":0,"k34":0,"k35":0,"k36":0,"k37":0,"k38":0,"k39":0,"k40":0,"k41":0,"k42":0,"k43":0,"k44":0,"k45":0,"k46":0,"k47":0,"k48":0,"k49":0,"k50":0,"k51":0,"k52":0,"k53":0,"k54":0,"k55":0,"k56":0,"k57":0,"k58":0,"k59":0,"k60":0,"k61":0,"k62":0,"k63":0,"k64":0,"k65":0,"k66":0,"k67":0,"k68":0,"k69":0,"k70":0,"k71":0,"k72":0,"k73":0,"k74":0,"k75":0,"k76":0,"k77":0,"k78":0,"k79":0,"k80":0,"k81":0,"k82":0,"k83":0,"k84":0,"k85":0,"k86":0,"k87":0,"k88":0,"k89":0,"k90":0,"k91":0,"k92":0,"k93":0,"k94":0,"k95":0,"k96":0,"k97":0,"k98":0,"k99":0,"k100":0,"k101":0,"k102":0,"k103":0,"k104":0,"k105":0,"k106":0,"k107":0,"k108":0,"k109":0,"k110":0,"k111":0,"k112":0,"k113":0,"k114":0,"k115":0,"k116":0,"k117":0,"k118":0,"k119":0,"k120":0,"k121":0,"k122":0,"k123":0,"k124":0,"k125":0,"k126":0,"k127":0,"k128":0,"k129":0,"k130":0,"k131":0,"k132":0,"k133":0,"k134":0,"k135":0,"k136":0,"k137":0,"k138":0,"k139":0,"k140":0,"k141":0,"k142":0,"k143":0,"k144":0,"k145":0,"k146":0,"k147":0,"k148":0,"k149":0,"k150":0,"k151":0,"k152":0,"k153":0,"k154":0,"k155":0,"k156":0,"k157":0,"k158":0,"k159":0,"k160":0,"k161":0,"k162":0,"k163":0,"k164":0,"k165":0,"k166":0,"k167":0,"k168":0,"k169":0,"k170":0,"k171":0,"k172":0,"k173":0,"k174":0,"k175":0,"k176":0,"k177":0,"k178":0,"k179":0,"k180":0,"k181":0,"k182":0,"k183":0,"k184":0,"k185":0,"k186":0,"k187":0,"k188":0,"k189":0,"k190":0,"k191":0,"k192":0,"k193":0,"k194":0,"k195":0,"k196":0,"k197":0,"k198":0,"k199":0,"k200":0,"k201":0,"k202":0,"k203":0,"k204":0,"k205":0,"k206":0,"k207":0,"k208":0,"k209":0,"k210":0,"k211":0,"k212":0,"k213":0,"k214":0,"k215":0,"k216":0,"k217":0,"k218":0,"k219":0,"k220":0,"k221":0,"k222":0,"k223":0,"k224":0,"k225":0,"k226":0,"k227":0,"k228":0,"k229":0,"k230":0,"k231":0,"k232":0,"k233":0,"k234":0,"k235":0,"k236":0,"k237":0,"k238":0,"k239":0,"k240":0,"k241":0,"k242":0,"k243":0,"k244":0,"k245":0,"k246":0,"k247":0,"k248":0,"k249":0,"k250":0,"k251":0,"k252":0,"k253":0,"k254":0},"blocks":[{"kind":"divider"}],"sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/115-parse-json-malformed/input.json b/src/test/resources/corpus/vectors/115-parse-json-malformed/input.json new file mode 100644 index 0000000..98cbc57 --- /dev/null +++ b/src/test/resources/corpus/vectors/115-parse-json-malformed/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","sig":,} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/120-spec-version-wrong/input.json b/src/test/resources/corpus/vectors/120-spec-version-wrong/input.json new file mode 100644 index 0000000..3c894c2 --- /dev/null +++ b/src/test/resources/corpus/vectors/120-spec-version-wrong/input.json @@ -0,0 +1 @@ +{"spec_version":"1.1","kind":"manifest","sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/121-kind-unknown/input.json b/src/test/resources/corpus/vectors/121-kind-unknown/input.json new file mode 100644 index 0000000..ff65408 --- /dev/null +++ b/src/test/resources/corpus/vectors/121-kind-unknown/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"unknown","sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/122-kind-missing-fields/input.json b/src/test/resources/corpus/vectors/122-kind-missing-fields/input.json new file mode 100644 index 0000000..cb8bcb2 --- /dev/null +++ b/src/test/resources/corpus/vectors/122-kind-missing-fields/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/130-schema-unknown-field/input.json b/src/test/resources/corpus/vectors/130-schema-unknown-field/input.json new file mode 100644 index 0000000..388c3be --- /dev/null +++ b/src/test/resources/corpus/vectors/130-schema-unknown-field/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA","unexpected_field":"x"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/131-schema-missing-required/input.json b/src/test/resources/corpus/vectors/131-schema-missing-required/input.json new file mode 100644 index 0000000..e76c0d1 --- /dev/null +++ b/src/test/resources/corpus/vectors/131-schema-missing-required/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/132-schema-null-value/input.json b/src/test/resources/corpus/vectors/132-schema-null-value/input.json new file mode 100644 index 0000000..a2b9bb7 --- /dev/null +++ b/src/test/resources/corpus/vectors/132-schema-null-value/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":null,"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/133-schema-block-kind-unknown/input.json b/src/test/resources/corpus/vectors/133-schema-block-kind-unknown/input.json new file mode 100644 index 0000000..1208086 --- /dev/null +++ b/src/test/resources/corpus/vectors/133-schema-block-kind-unknown/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/articles/bad-block","meta":{"title":"Bad block","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"marquee","content":"scrolling text"}],"sig":"k9Io3LyL82GMnsSQfuTVKYIZ3ifBD5JwRbFAFPyHj2UT98SMcon_fA_5gTjO2DUaK-2ddIudBjqQA8H61A-tBw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/134-schema-field-type/input.json b/src/test/resources/corpus/vectors/134-schema-field-type/input.json new file mode 100644 index 0000000..6a43f16 --- /dev/null +++ b/src/test/resources/corpus/vectors/134-schema-field-type/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":"3600","updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/135-schema-field-range/input.json b/src/test/resources/corpus/vectors/135-schema-field-range/input.json new file mode 100644 index 0000000..58a4834 --- /dev/null +++ b/src/test/resources/corpus/vectors/135-schema-field-range/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/articles/bad-range","meta":{"title":"Bad range","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"heading","level":7,"content":[{"kind":"text","value":"Too deep","marks":[]}]}],"sig":"Vxu7gP1FgocPWOV1q2DOk3_dfHcu3bdxLEG8QbLXvl-XyzENzkGUor08aXzvduZZhFWl9SgDEAj8Ij7boqO5CQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/136-schema-block-not-permitted/input.json b/src/test/resources/corpus/vectors/136-schema-block-not-permitted/input.json new file mode 100644 index 0000000..9151143 --- /dev/null +++ b/src/test/resources/corpus/vectors/136-schema-block-not-permitted/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"transaction","in_response_to":"/contact","request_id":"AAECAwQFBgcICQoLDA0ODw","request_hash":"sha-256:8iwqtPvCEOXMfs9B2hiPeJQRwfoqwH3myAUjbhpvZjQ","state_updates":[],"blocks":[{"kind":"submit_form","label":[{"kind":"text","value":"Send a message","marks":[]}],"submit_to":"/contact","fields":[{"kind":"textarea","name":"message","label":"Message","required":true,"max_length":1000}],"submit_label":"Send"}],"sig":"Z8ruEAVi3OBkN0d3Y0BVTiQ0rC3kD6m0W4Unk0EkRXFEtB78lJSC9gje6ppxH7n0LmyTA_ahWiWW2Q27UCGjBQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/136-schema-block-not-permitted/submit_body.json b/src/test/resources/corpus/vectors/136-schema-block-not-permitted/submit_body.json new file mode 100644 index 0000000..b88b448 --- /dev/null +++ b/src/test/resources/corpus/vectors/136-schema-block-not-permitted/submit_body.json @@ -0,0 +1 @@ +{"fields":{},"request_state":[],"request_id":"AAECAwQFBgcICQoLDA0ODw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/137-schema-duplicate-entry/input.json b/src/test/resources/corpus/vectors/137-schema-duplicate-entry/input.json new file mode 100644 index 0000000..931eeda --- /dev/null +++ b/src/test/resources/corpus/vectors/137-schema-duplicate-entry/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[{"namespace":"session","key":"auth","mode":"request","max_size":512,"max_lifetime":86400,"purpose":"First entry."},{"namespace":"session","key":"auth","mode":"client_only","max_size":256,"max_lifetime":7776000,"purpose":"Duplicate (namespace, key)."}],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"pxRy-xFffpIEQuWLJ45h-1Pz_7KLR2VJcCzhkcT0StP0NoGBCXh0sP8aaOaA2lhxQy2Jl1ZtokAh-BJnU29xDw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/138-schema-malformed-unicode/input.json b/src/test/resources/corpus/vectors/138-schema-malformed-unicode/input.json new file mode 100644 index 0000000..d4c4431 --- /dev/null +++ b/src/test/resources/corpus/vectors/138-schema-malformed-unicode/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"Lone surrogate: \uD800."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/139-schema-field-length/input.json b/src/test/resources/corpus/vectors/139-schema-field-length/input.json new file mode 100644 index 0000000..6b402b0 --- /dev/null +++ b/src/test/resources/corpus/vectors/139-schema-field-length/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received.","freshness_proof":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"8sKrpt-VMNnjmqLJv3kzNfUbC2a3JmSkOueDFKJQWFt6jqQG5U3ig7-uDiEUxDV7eVKWzLYfnE6Np7U1jUdJCg"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/140-numeric-float/input.json b/src/test/resources/corpus/vectors/140-numeric-float/input.json new file mode 100644 index 0000000..8a92690 --- /dev/null +++ b/src/test/resources/corpus/vectors/140-numeric-float/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","min_refresh_interval":3600.0,"sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/141-numeric-exponent/input.json b/src/test/resources/corpus/vectors/141-numeric-exponent/input.json new file mode 100644 index 0000000..6aa48d5 --- /dev/null +++ b/src/test/resources/corpus/vectors/141-numeric-exponent/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","min_refresh_interval":3.6e3,"sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/142-numeric-overflow/input.json b/src/test/resources/corpus/vectors/142-numeric-overflow/input.json new file mode 100644 index 0000000..75b45a0 --- /dev/null +++ b/src/test/resources/corpus/vectors/142-numeric-overflow/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":9223372036854775808,"updated":"2026-05-07T00:00:00Z","sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/143-submit-budget-state-overflow/input.json b/src/test/resources/corpus/vectors/143-submit-budget-state-overflow/input.json new file mode 100644 index 0000000..4faec4f --- /dev/null +++ b/src/test/resources/corpus/vectors/143-submit-budget-state-overflow/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[{"namespace":"ns","key":"k00","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k01","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k02","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k03","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k04","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k05","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k06","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k07","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k08","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k09","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k10","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k11","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k12","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k13","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k14","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k15","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k16","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k17","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k18","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k19","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k20","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k21","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k22","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k23","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k24","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k25","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k26","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k27","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k28","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k29","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k30","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."},{"namespace":"ns","key":"k31","mode":"request","max_size":2048,"max_lifetime":86400,"purpose":"Aggregate overflow probe."}],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"R-nzwPNZ8ogibDKzdSNkei6BHtLZel92iTNrX_QuHwcrOLXRRG8rnHItSI0qMR0PWD5LWEGXgEgmCGndkiDcAw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/150-sig-modified-payload/input.json b/src/test/resources/corpus/vectors/150-sig-modified-payload/input.json new file mode 100644 index 0000000..14b9fe1 --- /dev/null +++ b/src/test/resources/corpus/vectors/150-sig-modified-payload/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3601,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/151-sig-syntax-length/input.json b/src/test/resources/corpus/vectors/151-sig-syntax-length/input.json new file mode 100644 index 0000000..ddd8899 --- /dev/null +++ b/src/test/resources/corpus/vectors/151-sig-syntax-length/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/152-sig-non-canonical-s/input.json b/src/test/resources/corpus/vectors/152-sig-non-canonical-s/input.json new file mode 100644 index 0000000..2c493f5 --- /dev/null +++ b/src/test/resources/corpus/vectors/152-sig-non-canonical-s/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAnlr9MsxmoLRfnWUoKK4iyyYs4dPl4ZF00rSv1ntV3FEA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/153-sig-small-order-pubkey/input.json b/src/test/resources/corpus/vectors/153-sig-small-order-pubkey/input.json new file mode 100644 index 0000000..acecc64 --- /dev/null +++ b/src/test/resources/corpus/vectors/153-sig-small-order-pubkey/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/154-sig-non-canonical-r/input.json b/src/test/resources/corpus/vectors/154-sig-non-canonical-r/input.json new file mode 100644 index 0000000..b19bc69 --- /dev/null +++ b/src/test/resources/corpus/vectors/154-sig-non-canonical-r/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"___________________________________________4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/155-sig-non-canonical-a/input.json b/src/test/resources/corpus/vectors/155-sig-non-canonical-a/input.json new file mode 100644 index 0000000..001afcc --- /dev/null +++ b/src/test/resources/corpus/vectors/155-sig-non-canonical-a/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"__________________________________________8","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/156-sig-invalid-key-no-manifest/input.json b/src/test/resources/corpus/vectors/156-sig-invalid-key-no-manifest/input.json new file mode 100644 index 0000000..f21da18 --- /dev/null +++ b/src/test/resources/corpus/vectors/156-sig-invalid-key-no-manifest/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/articles/orphan-content","meta":{"title":"First post","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"paragraph","content":[{"kind":"text","value":"Hello, world.","marks":[]}]}],"sig":"e-QfgQweOxOCb62dE5PkBeWeF01r8IO_TUr9VXQkSUbvSUn0L5ZW8-AKgEKT_qwnDfRwShkxfieScbQHRTdsDQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/157-sig-small-order-r/input.json b/src/test/resources/corpus/vectors/157-sig-small-order-r/input.json new file mode 100644 index 0000000..8611b35 --- /dev/null +++ b/src/test/resources/corpus/vectors/157-sig-small-order-r/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/160-base64url-padded/input.json b/src/test/resources/corpus/vectors/160-base64url-padded/input.json new file mode 100644 index 0000000..ba5168f --- /dev/null +++ b/src/test/resources/corpus/vectors/160-base64url-padded/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA=="} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/161-base64url-standard-alphabet/input.json b/src/test/resources/corpus/vectors/161-base64url-standard-alphabet/input.json new file mode 100644 index 0000000..c76b8b1 --- /dev/null +++ b/src/test/resources/corpus/vectors/161-base64url-standard-alphabet/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw+4ii7+1tV5VAn4293Pqwf57CI6W9+r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/162-base64url-whitespace/input.json b/src/test/resources/corpus/vectors/162-base64url-whitespace/input.json new file mode 100644 index 0000000..6d4fc90 --- /dev/null +++ b/src/test/resources/corpus/vectors/162-base64url-whitespace/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn 4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/170-bind-path-mismatch/input.json b/src/test/resources/corpus/vectors/170-bind-path-mismatch/input.json new file mode 100644 index 0000000..833224c --- /dev/null +++ b/src/test/resources/corpus/vectors/170-bind-path-mismatch/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/articles/foo","meta":{"title":"First post","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"paragraph","content":[{"kind":"text","value":"Hello, world.","marks":[]}]}],"sig":"7Amma0jUMLe5jWj7HRt5tUeWCkuKEsFaBDy4xmIXKToDd1jKYSBTLeyaOajxHQX5UXs1ioqY_o0w_8qMhNmkBg"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/171-bind-reserved-manifest-path/input.json b/src/test/resources/corpus/vectors/171-bind-reserved-manifest-path/input.json new file mode 100644 index 0000000..46d6b34 --- /dev/null +++ b/src/test/resources/corpus/vectors/171-bind-reserved-manifest-path/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"content","path":"/manifest.json","meta":{"title":"First post","published_at":"2026-05-07T00:00:00Z"},"blocks":[{"kind":"paragraph","content":[{"kind":"text","value":"Hello, world.","marks":[]}]}],"sig":"uI_SgGRLVRCVpBkCTJGX-yf0xDwBpmbo4AuLW-fK16HqW6Yp-8de_o1mH7mfpdZCnqIPxMfSyXN_RStapOVXBA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/172-bind-request-hash-mismatch/input.json b/src/test/resources/corpus/vectors/172-bind-request-hash-mismatch/input.json new file mode 100644 index 0000000..0242176 --- /dev/null +++ b/src/test/resources/corpus/vectors/172-bind-request-hash-mismatch/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"transaction","in_response_to":"/contact","request_id":"AAECAwQFBgcICQoLDA0ODw","request_hash":"sha-256:-EvECkoil9nNYYBfRQE85W5pWojAP0K9UG830mtQn0M","state_updates":[],"blocks":[{"kind":"feedback","variant":"success","content":[{"kind":"text","value":"Received.","marks":[]}]}],"sig":"n8b1lkrg62RQB8ZGDsJbvU_U3m5qQXaVyZPYasnQdG9VhT9Cyvw0JOFZaQMGzhNkCq0rt-Apmpezxpvj7DIBDw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/172-bind-request-hash-mismatch/submit_body.json b/src/test/resources/corpus/vectors/172-bind-request-hash-mismatch/submit_body.json new file mode 100644 index 0000000..6f2c8a1 --- /dev/null +++ b/src/test/resources/corpus/vectors/172-bind-request-hash-mismatch/submit_body.json @@ -0,0 +1 @@ +{"fields":{"message":"TAMPERED","name":"alice"},"request_state":[],"request_id":"AAECAwQFBgcICQoLDA0ODw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/175-bind-origin/input.json b/src/test/resources/corpus/vectors/175-bind-origin/input.json new file mode 100644 index 0000000..15ae176 --- /dev/null +++ b/src/test/resources/corpus/vectors/175-bind-origin/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/176-origin-invalid/input.json b/src/test/resources/corpus/vectors/176-origin-invalid/input.json new file mode 100644 index 0000000..69d5be0 --- /dev/null +++ b/src/test/resources/corpus/vectors/176-origin-invalid/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo","not_after":"2026-05-07T00:00:00Z"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"Hr4YHUow3LW1xr-DdrqEtFuSyNMj7mJjUEwUASIDtyNEYfxgrK6nvSPY0MRPgsUBTaLGfPzF3CyD3_jiT6wOAQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/177-origin-invalid-beyond-5y/input.json b/src/test/resources/corpus/vectors/177-origin-invalid-beyond-5y/input.json new file mode 100644 index 0000000..6997a9b --- /dev/null +++ b/src/test/resources/corpus/vectors/177-origin-invalid-beyond-5y/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo","not_after":"2031-05-08T00:00:00Z"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"aFTElLfXo-5SDSSa0Pd6Obc61MddYqT0eXqivpJMjofpfvFR2hD55ezHlHpuIIgmugikgB0gaN4q0wJyZZA1DA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/178-manifest-updated-future-skew/input.json b/src/test/resources/corpus/vectors/178-manifest-updated-future-skew/input.json new file mode 100644 index 0000000..da03111 --- /dev/null +++ b/src/test/resources/corpus/vectors/178-manifest-updated-future-skew/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:07:00Z","sig":"ji8jStK9wzGg0hw0uVzpDR4vsTZcSFmp9gmqC6P7E78lGxiup4UDi7TXFMUX1PC1-DCDA93pisYFmRPlKGXvBQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/180-canary-equal-issued-at-conflict/input.json b/src/test/resources/corpus/vectors/180-canary-equal-issued-at-conflict/input.json new file mode 100644 index 0000000..00de236 --- /dev/null +++ b/src/test/resources/corpus/vectors/180-canary-equal-issued-at-conflict/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"rN5KJ_PD7wcccfXJTgJVZIRG5bd8O-ZYhgdImGhD980","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"TXbXg4lKFs1XG62m7lX81y4rFj-ZdPhCs2nVBQd2sM6kD2KQfmYcFt5Y0RXDTtjcv3_rhMwOeZRpemRIlhhMCw"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/181-canary-issued-at-downgrade/input.json b/src/test/resources/corpus/vectors/181-canary-issued-at-downgrade/input.json new file mode 100644 index 0000000..7e8299e --- /dev/null +++ b/src/test/resources/corpus/vectors/181-canary-issued-at-downgrade/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-04-01T00:00:00Z","next_expected":"2026-05-01T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-04-01T00:00:00Z","sig":"Ck7RplR4fjqAVOyU3d8U6Oy0k2a1ZrYPOlzGedKzhRbid4PkhO18y9tiZkri6DfsK0CzlTA6jdiecGj2NkysAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/182-canary-invalid/input.json b/src/test/resources/corpus/vectors/182-canary-invalid/input.json new file mode 100644 index 0000000..25861e0 --- /dev/null +++ b/src/test/resources/corpus/vectors/182-canary-invalid/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-06T00:00:00Z","next_expected":"2026-05-12T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-06T00:00:00Z","sig":"sFswJMMZ3_QTx1GD12X0mWLuz5LfHK26qGdVlul55MFU2wxApKIc90MgVpGVEnhKMTBbmsRcTWeUpqXoO2JyDQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/183-canary-issued-at-future-skew/input.json b/src/test/resources/corpus/vectors/183-canary-issued-at-future-skew/input.json new file mode 100644 index 0000000..b8966f2 --- /dev/null +++ b/src/test/resources/corpus/vectors/183-canary-issued-at-future-skew/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:07:00Z","next_expected":"2026-06-06T00:07:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"V_nta98RZyIvADb2bOh0nJ3RE2Do6Yj11_OPh4Dby_Msp6dTwJmFBx7r-YXbGQKW-H18SiZ9c6sBAh-QHSKIDA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/184-canary-runtime-reuse/input.json b/src/test/resources/corpus/vectors/184-canary-runtime-reuse/input.json new file mode 100644 index 0000000..15ae176 --- /dev/null +++ b/src/test/resources/corpus/vectors/184-canary-runtime-reuse/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/184-canary-runtime-reuse/prior_manifest.json b/src/test/resources/corpus/vectors/184-canary-runtime-reuse/prior_manifest.json new file mode 100644 index 0000000..ecbc9d9 --- /dev/null +++ b/src/test/resources/corpus/vectors/184-canary-runtime-reuse/prior_manifest.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-04-30T00:00:00Z","next_expected":"2026-05-30T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-04-30T00:00:00Z","sig":"TSN-whnRBa_mBwYzgzcTJIYWtkI0CYDhNA30xRWo99aiLeZg3LipDHrNvNaneNSrlDrVhkL7CH2ArDhOx9bqDQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/input.json b/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/input.json new file mode 100644 index 0000000..15ae176 --- /dev/null +++ b/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"lxqKzft7mV16fVo0hJMVTw2EL85cyw-4ii7-1tV5VAn4293Pqwf57CI6W9-r6E2dYs4dPl4ZF00rSv1ntV3FAA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/prior_manifest_a.json b/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/prior_manifest_a.json new file mode 100644 index 0000000..f2e729b --- /dev/null +++ b/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/prior_manifest_a.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-04-23T00:00:00Z","next_expected":"2026-05-23T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-04-23T00:00:00Z","sig":"Zf4yQ1wPuwPnBkPTV9gVWyFHVd_fRW8a0AguTxT6Tk4wHEbiewi8Xj9-1MRuzzxZOcYy8rmZ1tjR9Hd0_N2zDg"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/prior_manifest_b.json b/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/prior_manifest_b.json new file mode 100644 index 0000000..c331a60 --- /dev/null +++ b/src/test/resources/corpus/vectors/185-canary-runtime-reuse-resurrection/prior_manifest_b.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"rN5KJ_PD7wcccfXJTgJVZIRG5bd8O-ZYhgdImGhD980","issued_at":"2026-04-30T00:00:00Z","next_expected":"2026-05-30T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-04-30T00:00:00Z","sig":"gVRvkIUyJk3C0XpPun90WJCEiJTxRyUytmGYe_tugO7X0KyMTwuMri4IMGRtn4vhX1dF_oyaUj7yVWFPnUU8DA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/190-unicode-nfd-statement/input.json b/src/test/resources/corpus/vectors/190-unicode-nfd-statement/input.json new file mode 100644 index 0000000..c76d2a8 --- /dev/null +++ b/src/test/resources/corpus/vectors/190-unicode-nfd-statement/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"Café"},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"6diwyaPqe1QOGmzcPxsQXOoVHUkqMkxcvAsXBkJWU8Rmh5La81PXITMLFUZAmfMqOwVZcyTBMGmQSg8kxVfvAQ"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/191-unicode-nfd-freshness-proof/input.json b/src/test/resources/corpus/vectors/191-unicode-nfd-freshness-proof/input.json new file mode 100644 index 0000000..e18abf5 --- /dev/null +++ b/src/test/resources/corpus/vectors/191-unicode-nfd-freshness-proof/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received.","freshness_proof":"Café block-871234"},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","sig":"pMKnVqHYC7cMsCyE6Rc6KOAPl6pYGyggZPUvf8YOHNzqgGk4_SnK6Hon_V6nTxagtP3Y9FiHVMvRMWy_SztlDA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/200-migration-successor-origin-expired/input.json b/src/test/resources/corpus/vectors/200-migration-successor-origin-expired/input.json new file mode 100644 index 0000000..4388ddb --- /dev/null +++ b/src/test/resources/corpus/vectors/200-migration-successor-origin-expired/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","migration_pointer":{"successor_origin":{"carrier":"tor-v3","address":"fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion","origin_pubkey":"Ko4-lXCoSgOUc_2Dt2Bi9NcyVyUN3GYgEKMJkgPQtb8"},"announced_at":"2026-05-07T00:00:00Z"},"sig":"CWXJMdKqJa9--8_qNdGqlVBkGMTQuIneowJ7ohWYXXRSqg2UDSkF8cUbKQWL8Yho7wQXmghFIxVS0d8CgQpBBA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/200-migration-successor-origin-expired/successor_manifest.json b/src/test/resources/corpus/vectors/200-migration-successor-origin-expired/successor_manifest.json new file mode 100644 index 0000000..b93be93 --- /dev/null +++ b/src/test/resources/corpus/vectors/200-migration-successor-origin-expired/successor_manifest.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion","origin_pubkey":"Ko4-lXCoSgOUc_2Dt2Bi9NcyVyUN3GYgEKMJkgPQtb8","not_after":"2026-05-01T00:00:00Z"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-04-01T00:00:00Z","next_expected":"2026-05-01T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-04-01T00:00:00Z","sig":"EOI7imul8ztDJ84T1oHp8yVfoisrw-5aRuJrIDwc3REmClR4H4r390AH7FUm2XRQkrCMy6M1-f00eykBZxfNDg"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/201-migration-chain-cycle/input.json b/src/test/resources/corpus/vectors/201-migration-chain-cycle/input.json new file mode 100644 index 0000000..4388ddb --- /dev/null +++ b/src/test/resources/corpus/vectors/201-migration-chain-cycle/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","migration_pointer":{"successor_origin":{"carrier":"tor-v3","address":"fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion","origin_pubkey":"Ko4-lXCoSgOUc_2Dt2Bi9NcyVyUN3GYgEKMJkgPQtb8"},"announced_at":"2026-05-07T00:00:00Z"},"sig":"CWXJMdKqJa9--8_qNdGqlVBkGMTQuIneowJ7ohWYXXRSqg2UDSkF8cUbKQWL8Yho7wQXmghFIxVS0d8CgQpBBA"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/201-migration-chain-cycle/successor_manifest.json b/src/test/resources/corpus/vectors/201-migration-chain-cycle/successor_manifest.json new file mode 100644 index 0000000..43c2371 --- /dev/null +++ b/src/test/resources/corpus/vectors/201-migration-chain-cycle/successor_manifest.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion","origin_pubkey":"Ko4-lXCoSgOUc_2Dt2Bi9NcyVyUN3GYgEKMJkgPQtb8"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","migration_pointer":{"successor_origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"announced_at":"2026-05-07T00:00:00Z"},"sig":"Kq2uvJJncCv4xbFAb1RKVPpebCTxARyUra5a4eC93Io5e71I39co2-xxAN-bYbRxegsXRxAlRLbw3FiobMJHCg"} \ No newline at end of file diff --git a/src/test/resources/corpus/vectors/202-migration-successor-key-mismatch/input.json b/src/test/resources/corpus/vectors/202-migration-successor-key-mismatch/input.json new file mode 100644 index 0000000..5584796 --- /dev/null +++ b/src/test/resources/corpus/vectors/202-migration-successor-key-mismatch/input.json @@ -0,0 +1 @@ +{"spec_version":"1.0","kind":"manifest","publisher_pubkey":"moyzpl3i5hUIcMNRLPMxir4sdmSO3gO79gLUtvYDWxc","origin":{"carrier":"tor-v3","address":"dkptfyethnbfsj7qsxscia4w6lg4yssjca2gdrqlk457qav2lkna4xqd.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"canary":{"runtime_pubkey":"jzFtziEJkbIdjI15I4u3ni3bBa6IFElyyjEmMVSGF7o","issued_at":"2026-05-07T00:00:00Z","next_expected":"2026-06-06T00:00:00Z","statement":"No warrants received."},"state_policy":[],"navigation":[],"min_refresh_interval":3600,"updated":"2026-05-07T00:00:00Z","migration_pointer":{"successor_origin":{"carrier":"tor-v3","address":"fkhd5flqvbfahfdt7wb3oydc6tltevzfbxogmiaqumezea6qww7rjhid.onion","origin_pubkey":"Gp8y4JM7Qlkn8JXkJAOW8s3MSkkQNGHGC1c7-AK6Wpo"},"announced_at":"2026-05-07T00:00:00Z"},"sig":"DLb4SdASULu9_t9wpOOsw7PRjbHwysMzukd-LIC08eRbAVmRktzSeK7OOgasS9wvhI5rk_d6zX7W76Gi2MI7Cg"} \ No newline at end of file