diff --git a/Cargo.lock b/Cargo.lock index 6bb48e2a..38abf16d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,7 @@ dependencies = [ "csv", "encoding_rs", "flate2", + "html5ever", "insta", "log", "pdf-inspector", @@ -668,6 +669,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "html5ever" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456a1a377e608e555d22ddab27ac0114bc7a7b4199078108e34c2aeae6c9b130" +dependencies = [ + "log", + "markup5ever", + "memchr", +] + [[package]] name = "hybrid-array" version = "0.4.14" @@ -850,6 +862,15 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.33" @@ -887,6 +908,17 @@ dependencies = [ "weezl", ] +[[package]] +name = "markup5ever" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab3dc68ac4a0f5719e560136778c1ee716e296030d75dbd4484e37e39e3a842" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + [[package]] name = "md-5" version = "0.10.6" @@ -970,6 +1002,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -1012,6 +1050,29 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pdf-inspector" version = "1.14.2" @@ -1030,6 +1091,45 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1057,6 +1157,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1190,6 +1296,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "regex" version = "1.13.1" @@ -1250,6 +1365,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.28" @@ -1347,12 +1468,48 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "string_cache" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa8a5dbe8b3f0bbe29d4c3225daafaeead63afdc1b65fc4c01a1384166038e6" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928dcdf75e47626b3617a976ec205d9f057584c371c1f23b782129268d0e6edc" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -1405,6 +1562,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + [[package]] name = "thiserror" version = "2.0.19" @@ -1598,6 +1764,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7572660c8890448ba236b7376f27e389c6a7e1c70195622faced601f855c0ada" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "weezl" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index 2a334b72..9529c69d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ version = "0.2.4" edition = "2024" # Edition 2024 needs 1.85; zip raises it to 1.88. rust-version = "1.88" -description = "Convert documents (doc, docx, odt, rtf, epub, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown" +description = "Convert documents (doc, docx, odt, rtf, epub, chm, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown" license = "MIT" repository = "https://github.com/firecrawl/anydoc" readme = "README.md" @@ -26,6 +26,7 @@ cfb = "0.14.0" csv = "1.4.0" flate2 = "1" encoding_rs = "0.8.35" +html5ever = "0.40" log = "0.4" pdf-inspector = "1.14.2" quick-xml = "0.41.0" diff --git a/README.md b/README.md index 760d3f18..f7a74746 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![skills.sh](https://skills.sh/b/firecrawl/anydoc)](https://skills.sh/firecrawl/anydoc) -Fast Rust library that converts documents (Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF) into clean GitHub-Flavored Markdown. Includes bindings for [Node.js](node/README.md), [Python](python/README.md), and the [browser](wasm/README.md) (WebAssembly). +Fast Rust library that converts documents (Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CHM, CSV, and PDF) into clean GitHub-Flavored Markdown. Includes bindings for [Node.js](node/README.md), [Python](python/README.md), and the [browser](wasm/README.md) (WebAssembly). Built by [Firecrawl](https://firecrawl.dev) to turn any office document into LLM-ready Markdown in single-digit milliseconds, with one consistent output no matter which format goes in. It powers [Firecrawl Parse](https://firecrawl.dev/parse), so if you'd rather not run it yourself, the hosted API gives you the same conversion plus our OCR models for the scanned pages anydoc can't read on its own. @@ -166,9 +166,20 @@ Only documents that need OCR leave the machine, and the whole document goes, sin | OpenDocument | `.odt`, `.ods`, `.odp` | | Rich Text Format | `.rtf` | | EPUB | `.epub` | +| Compiled HTML Help | `.chm` | | CSV | `.csv` | | PDF | `.pdf` | +CHM topics are read in `.hhc` table-of-contents order, with the hierarchy +preserved as a nested list. Unlisted HTML topics are appended in stable path +order; the same fallback applies when the TOC is missing, unreadable, or binary. +Legacy HTML uses BOM/charset declarations, then the book's Windows language +encoding (including GBK/Big5). Topic links, HTML bases, and stylesheet encodings +are resolved locally. LZX supports 4/8-byte reset tables and sequential SpanInfo +streams; the private decoder adaptation and its MIT notice are under +`src/formats/chm/lzx/`. Embedded images remain available on `Document.assets`, +as with the other formats. + ## Benchmark anydoc is measured against six other converters on 100 real-world documents spanning fourteen formats. Scores run from 0 to 100, higher is better; speed is the median time to convert one document. @@ -209,7 +220,7 @@ Speed is one warm conversion per document on a Ryzen 9 9950X3D (Windows 11, 64 G ## Format detection -The format is read from the file content, using the marker its specification designates: the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype and content types. CSV has no such marker, so the extension or an explicit format names it instead. +The format is read from the file content, using the marker its specification designates: the CHM `ITSF` signature, the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype and content types. CSV has no such marker, so the extension or an explicit format names it instead. ```rust Format::from_bytes(&bytes); // Some(Format::Docx), or None when nothing matches @@ -261,7 +272,7 @@ document bytes ├─► format detection → content markers, not the extension │ ├─► format parser → one per format (doc, docx, ppt, pptx, xls, - │ xlsx, odt/ods/odp, rtf, epub, csv) + │ xlsx, odt/ods/odp, rtf, epub, chm, csv) │ │ │ └─► Document → shared model: blocks, inlines, tables, │ footnotes, assets @@ -284,6 +295,13 @@ wasm-pack build wasm --release --target web --scope firecrawl && node --test was A committed fixture corpus under `tests/fixtures/` is snapshot-tested, `tests/robustness.rs` mutation-tests every fixture, and `fuzz/` carries cargo-fuzz targets per format. The speed and quality benchmark lives in [`bench/`](bench/README.md). +The small CHM fixtures can be rebuilt with `python3 tests/fixture-src/chm.py`. +To run the local ZXR10 manual regression without adding the manual to the repo: + +```bash +ANYDOC_CHM_SAMPLE=/path/to/manual.chm cargo test --test chm local_chm_manual -- --ignored --nocapture +``` + Releases are tagged `v`, which publishes the crate, the npm package, and the PyPI wheels from [`.github/workflows/release.yml`](.github/workflows/release.yml). The version lives in three places, bumped together for a release: - [`Cargo.toml`](Cargo.toml): the crate diff --git a/bench/README.md b/bench/README.md index 04471ec6..40e9c6b6 100644 --- a/bench/README.md +++ b/bench/README.md @@ -2,7 +2,8 @@ Benchmarks anydoc against well-known document-to-markdown converters on the `samples/` corpus, for both speed and quality (deterministic metrics + LLM judge). -Office, text, and presentation documents; PDFs are out of scope. +Office, text, presentation, and CHM documents; PDFs are out of scope. CHM runs +only against anydoc; the competitor format lists are unchanged. The corpus is not redistributable and is not in the repo. The harness reads whatever documents are in `samples/`. @@ -48,6 +49,33 @@ python report.py # aggregates everything into out/report.m All artifacts land in `out/` (gitignored). `convert.py` appends to `out/timings.jsonl`; `report.py` keeps only the latest row per (tool, file). +## CHM time and memory + +The standard harness accepts `--formats chm`. For per-process peak memory and +repeatable comparisons with an external CHM, use the dependency-free macOS/Linux +runner from the repository root: + +``` +cargo build --release --example convert +python3 -m unittest discover -s bench -p test_chm.py +python3 bench/chm.py /path/to/book.chm --warmup 1 --iters 3 +python3 bench/chm.py /path/to/book.chm --binary /path/to/baseline/convert +``` + +`chm.py` prints one JSONL record with every run and their medians. Redirect stdout +to retain a local result. It never copies the input or saves converted contents. +Each warmup and measured run starts a fresh converter process; warmups warm OS +caches without retaining converter state. Run comparisons serially on the same +machine/build settings. +`conversion_ms` is the CLI's rounded API time (CHM to Markdown, including the +document model); `wall_ms` includes process startup, input I/O, conversion, writing +Markdown to the null device, and process teardown. Assets are not extracted. +`peak_rss_bytes` is the converter process's peak resident set from `wait4`, not +total allocated memory or the Python runner's memory: macOS reports bytes, Linux +reports KiB, normalized here to bytes. Results include binary path, platform, and +input size; record the tested commit/build separately. No wall-clock thresholds +are enforced in CI. Use a local private CHM path without adding it to the corpus. + ## Methodology notes - **Speed**: min of `--iters` warm runs, and the published numbers use `--iters 1`, diff --git a/bench/chm.py b/bench/chm.py new file mode 100644 index 00000000..cd106cee --- /dev/null +++ b/bench/chm.py @@ -0,0 +1,100 @@ +"""Measure release CHM conversion time and per-process peak RSS on macOS/Linux. + +Reads an external CHM without saving its converted contents. Writes one JSONL +record to stdout; see README.md for measurement boundaries. +""" + +import argparse +import json +import os +import platform +import re +import statistics +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def peak_rss_bytes(maxrss, platform_name): + # wait4's ru_maxrss is bytes on macOS, KiB on Linux. + if platform_name == "darwin": + return int(maxrss) + if platform_name == "linux": + return int(maxrss) * 1024 + raise ValueError("peak RSS measurement requires macOS or Linux") + + +def conversion_ms(stderr): + match = re.search(r"^converted .* in (\d+(?:\.\d+)?)ms$", stderr, re.MULTILINE) + if not match: + raise ValueError("anydoc did not report a conversion time") + return float(match.group(1)) + + +def measure(command): + # A file avoids pipe backpressure without retaining Markdown or stderr in + # the harness's memory while the converter runs. + with tempfile.TemporaryFile() as stderr: + start = time.perf_counter() + with subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=stderr) as process: + try: + # Reap this exact process once; Popen.wait() would discard rusage. + _, status, usage = os.wait4(process.pid, 0) + process.returncode = os.waitstatus_to_exitcode(status) + except BaseException: + process.kill() + raise + wall_ms = (time.perf_counter() - start) * 1000 + stderr.seek(0) + message = stderr.read().decode("utf-8", "replace").strip() + if process.returncode: + raise RuntimeError(message or f"converter exited with status {process.returncode}") + return { + "wall_ms": wall_ms, + "conversion_ms": conversion_ms(message), + "peak_rss_bytes": peak_rss_bytes(usage.ru_maxrss, sys.platform), + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", type=Path, help="path to an external CHM; never copied") + parser.add_argument("--binary", type=Path, default=ROOT / "target/release/examples/convert", + help="release convert binary, including a saved baseline build") + parser.add_argument("--iters", type=int, default=3) + parser.add_argument("--warmup", type=int, default=1) + args = parser.parse_args() + if sys.platform not in {"darwin", "linux"} or not hasattr(os, "wait4"): + parser.error("peak RSS measurement requires macOS or Linux") + if args.iters < 1 or args.warmup < 0: + parser.error("--iters must be at least 1 and --warmup must be nonnegative") + if not args.input.is_file() or args.input.suffix.lower() != ".chm": + parser.error("input must be an existing CHM file") + if not args.binary.is_file() or not os.access(args.binary, os.X_OK): + parser.error("release binary missing; run: cargo build --release --example convert") + + command = [str(args.binary.resolve()), str(args.input.resolve())] + try: + for _ in range(args.warmup): + measure(command) + runs = [measure(command) for _ in range(args.iters)] + except (OSError, RuntimeError, ValueError) as error: + parser.exit(1, f"error: {error}\n") + row = { + "tool": "anydoc", "format": "chm", "mode": "markdown", + "file": str(args.input.resolve()), "bytes": args.input.stat().st_size, + "binary": str(args.binary.resolve()), "platform": platform.platform(), + "machine": platform.machine(), "ts": time.time(), + "warmup": args.warmup, "iters": args.iters, "runs": runs, + } + for key in ("wall_ms", "conversion_ms", "peak_rss_bytes"): + row[f"median_{key}"] = statistics.median(run[key] for run in runs) + print(json.dumps(row)) + + +if __name__ == "__main__": + main() diff --git a/bench/convert.py b/bench/convert.py index ac2b6144..bb48be06 100644 --- a/bench/convert.py +++ b/bench/convert.py @@ -20,7 +20,7 @@ ROOT = BENCH.parent OUT = BENCH / "out" -ALL_FORMATS = {"doc", "docx", "docm", "odt", "rtf", "epub", "xls", "xlsx", "xlsm", "xlsb", "ods", "csv", "ppt", "pptx", "odp"} +ALL_FORMATS = {"doc", "docx", "docm", "odt", "rtf", "epub", "xls", "xlsx", "xlsm", "xlsb", "ods", "csv", "ppt", "pptx", "odp", "chm"} def ext(path: Path) -> str: @@ -231,7 +231,7 @@ def one(): "unstructured": ({"doc", "docx", "odt", "rtf", "epub", "xls", "xlsx", "csv", "ppt", "pptx"}, convert_unstructured), "mammoth": ({"docx"}, convert_mammoth), - "libreoffice": (ALL_FORMATS - {"epub", "csv"}, convert_libreoffice), + "libreoffice": (ALL_FORMATS - {"epub", "csv", "chm"}, convert_libreoffice), } diff --git a/bench/test_chm.py b/bench/test_chm.py new file mode 100644 index 00000000..93878ba7 --- /dev/null +++ b/bench/test_chm.py @@ -0,0 +1,46 @@ +"""Tests for CHM benchmark accounting; no timing thresholds.""" + +import sys +import unittest + +import chm +import convert + + +class ChmBenchmarkTests(unittest.TestCase): + def test_chm_is_only_advertised_for_anydoc(self): + self.assertIn("chm", convert.ALL_FORMATS) + self.assertEqual( + [tool for tool, (formats, _) in convert.TOOLS.items() if "chm" in formats], + ["anydoc"], + ) + + def test_peak_rss_platform_units(self): + self.assertEqual(chm.peak_rss_bytes(1234, "darwin"), 1234) + self.assertEqual(chm.peak_rss_bytes(1234, "linux"), 1234 * 1024) + with self.assertRaises(ValueError): + chm.peak_rss_bytes(1234, "win32") + + def test_cli_conversion_time(self): + self.assertEqual(chm.conversion_ms("converted a book.chm in 1.25ms\n"), 1.25) + self.assertEqual(chm.conversion_ms("converted a book.chm in 2001ms\n"), 2001) + with self.assertRaises(ValueError): + chm.conversion_ms("error: invalid archive\n") + + @unittest.skipUnless(sys.platform in {"darwin", "linux"}, "requires wait4 RSS") + def test_measure_waits_for_its_own_child(self): + script = "import sys; print('discarded'); print('converted test.chm in 1.25ms', file=sys.stderr)" + result = chm.measure([sys.executable, "-c", script]) + self.assertEqual(result["conversion_ms"], 1.25) + self.assertGreater(result["peak_rss_bytes"], 0) + self.assertGreater(result["wall_ms"], 0) + + @unittest.skipUnless(sys.platform in {"darwin", "linux"}, "requires wait4 RSS") + def test_measure_reports_failed_conversion(self): + script = "import sys; print('error: invalid archive', file=sys.stderr); sys.exit(2)" + with self.assertRaisesRegex(RuntimeError, "invalid archive"): + chm.measure([sys.executable, "-c", script]) + + +if __name__ == "__main__": + unittest.main() diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index b8ea2618..e6706574 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -13,6 +13,12 @@ anydoc = { path = ".." } cfb = "0.14.0" zip = { version = "8.6.0", default-features = false, features = ["deflate"] } +[[bin]] +name = "chm" +path = "fuzz_targets/chm.rs" +test = false +doc = false + [[bin]] name = "docx" path = "fuzz_targets/docx.rs" diff --git a/fuzz/fuzz_targets/chm.rs b/fuzz/fuzz_targets/chm.rs new file mode 100644 index 00000000..3bd53cf4 --- /dev/null +++ b/fuzz/fuzz_targets/chm.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let _ = anydoc::to_markdown_bytes(data, anydoc::Format::Chm); +}); diff --git a/node/README.md b/node/README.md index 74500241..a8f611f9 100644 --- a/node/README.md +++ b/node/README.md @@ -3,7 +3,7 @@ [![npm](https://img.shields.io/npm/v/@firecrawl/anydoc.svg)](https://www.npmjs.com/package/@firecrawl/anydoc) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/firecrawl/anydoc/blob/main/LICENSE) -Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF files into clean GitHub-Flavored Markdown. Node.js bindings for the [anydoc](https://github.com/firecrawl/anydoc) Rust crate, built by [Firecrawl](https://firecrawl.dev). Also available as a hosted API through [Firecrawl Parse](https://firecrawl.dev/parse), which adds our OCR models for the scanned pages anydoc can't read on its own. +Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CHM, CSV, and PDF files into clean GitHub-Flavored Markdown. Node.js bindings for the [anydoc](https://github.com/firecrawl/anydoc) Rust crate, built by [Firecrawl](https://firecrawl.dev). Also available as a hosted API through [Firecrawl Parse](https://firecrawl.dev/parse), which adds our OCR models for the scanned pages anydoc can't read on its own. Every format parses into one shared document model and renders through a single Markdown serializer, so headings, tables, lists, and footnotes come out the same no matter which format goes in. Conversion runs on the libuv thread pool and never blocks the event loop. TypeScript types ship with the package. @@ -21,6 +21,7 @@ npm install @firecrawl/anydoc | OpenDocument | `.odt`, `.ods`, `.odp` | | Rich Text Format | `.rtf` | | EPUB | `.epub` | +| Compiled HTML Help | `.chm` | | CSV | `.csv` | | PDF | `.pdf` | @@ -97,7 +98,7 @@ try { ## Format detection -The format is read from the file content, using the marker its specification designates: the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype and content types. CSV has no such marker, so detection returns `null` for it and the extension, or an explicit format, names it instead. +The format is read from the file content, using the marker its specification designates: the CHM ITSF header, the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype and content types. CSV has no such marker, so detection returns `null` for it and the extension, or an explicit format, names it instead. ```js formatFromBytes(bytes); // 'docx', or null when nothing matches diff --git a/node/cli.js b/node/cli.js index 5d7cd34c..74b84069 100644 --- a/node/cli.js +++ b/node/cli.js @@ -3,7 +3,7 @@ const { readFile, writeFile } = require('node:fs/promises') -const FORMATS = 'doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv' +const FORMATS = 'doc, docx, odt, pdf, ppt, pptx, rtf, epub, chm, xlsx, ods, odp, csv' const HELP = `anydoc: convert documents to GitHub-Flavored Markdown diff --git a/node/index.d.ts b/node/index.d.ts index e3e4fd72..3e950531 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -128,6 +128,7 @@ export declare const enum Format { pptx = 'pptx', rtf = 'rtf', epub = 'epub', + chm = 'chm', xlsx = 'xlsx', ods = 'ods', odp = 'odp', @@ -136,9 +137,10 @@ export declare const enum Format { /** * Detect the format from the content itself: the signature and identity each - * container specification designates (PDF header, RTF open group, OLE stream - * names, ZIP package mimetype/content types). Plain-text formats (CSV) carry - * no signature and return `null`; so does anything unrecognized. + * container specification designates (CHM ITSF header, PDF header, RTF open + * group, OLE stream names, ZIP package mimetype/content types). Plain-text + * formats (CSV) carry no signature and return `null`; so does anything + * unrecognized. */ export declare function formatFromBytes(bytes: Uint8Array): Format | null diff --git a/node/package.json b/node/package.json index fae9b054..80c9150f 100644 --- a/node/package.json +++ b/node/package.json @@ -1,7 +1,7 @@ { "name": "@firecrawl/anydoc", "version": "0.2.4", - "description": "Convert documents (doc, docx, odt, rtf, epub, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown", + "description": "Convert documents (doc, docx, odt, rtf, epub, chm, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown", "license": "MIT", "homepage": "https://github.com/firecrawl/anydoc#readme", "repository": { diff --git a/node/src/lib.rs b/node/src/lib.rs index c32bd79e..51c16b3e 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -26,6 +26,7 @@ pub enum Format { pptx, rtf, epub, + chm, xlsx, ods, odp, @@ -43,6 +44,7 @@ impl From for anydoc::Format { Format::pptx => anydoc::Format::Pptx, Format::rtf => anydoc::Format::Rtf, Format::epub => anydoc::Format::Epub, + Format::chm => anydoc::Format::Chm, Format::xlsx => anydoc::Format::Excel, Format::ods => anydoc::Format::Ods, Format::odp => anydoc::Format::Odp, @@ -62,6 +64,7 @@ impl From for Format { anydoc::Format::Pptx => Format::pptx, anydoc::Format::Rtf => Format::rtf, anydoc::Format::Epub => Format::epub, + anydoc::Format::Chm => Format::chm, anydoc::Format::Excel => Format::xlsx, anydoc::Format::Ods => Format::ods, anydoc::Format::Odp => Format::odp, @@ -71,9 +74,10 @@ impl From for Format { } /// Detect the format from the content itself: the signature and identity each -/// container specification designates (PDF header, RTF open group, OLE stream -/// names, ZIP package mimetype/content types). Plain-text formats (CSV) carry -/// no signature and return `null`; so does anything unrecognized. +/// container specification designates (CHM ITSF header, PDF header, RTF open +/// group, OLE stream names, ZIP package mimetype/content types). Plain-text +/// formats (CSV) carry no signature and return `null`; so does anything +/// unrecognized. #[napi] pub fn format_from_bytes(bytes: Uint8Array) -> Option { anydoc::Format::from_bytes(&bytes).map(Format::from) diff --git a/node/test.mjs b/node/test.mjs index 0d4f6a4b..7480eb11 100644 --- a/node/test.mjs +++ b/node/test.mjs @@ -25,6 +25,7 @@ const RICH = fixture('docx/handmade-rich.docx') const CSV = fixture('csv/sheet.csv') const ENCRYPTED = fixture('malformed/encrypted--errors.odt') const MIXED = fixture('pdf/handmade-mixed.pdf') +const CHM = fixture('chm/handmade-book.chm') test('toMarkdown detects the format from the file content', async () => { const markdown = await toMarkdown(OUTLINE) @@ -61,6 +62,32 @@ test('toDocument carries embedded assets as buffers', async () => { assert.equal(image.id, document.assets.indexOf(image)) }) +test('chm converts through the file, bytes, and document APIs', async () => { + const bytes = await readFile(CHM) + const markdown = await toMarkdownBytes(bytes) + assert.equal(await toMarkdown(CHM), markdown) + assert.equal(await toMarkdownBytes(bytes, 'chm'), markdown) + for (const title of ['使用指南', '命令参考', '补充说明']) assert.ok(markdown.includes(title)) + assert.match(markdown, /配置路由器/) + assert.match(markdown, /\| 命令 \| 说明 \|/) + assert.match(markdown, /show version/) + assert.match(markdown, /\[命令语法\]\(#[^)]+\)/) + + const document = await toDocument(bytes, 'chm') + assert.deepEqual(await toDocument(bytes), document) + assert.equal(document.blocks.filter((block) => block.kind === 'heading' && block.level === 1).length, 3) + assert.ok(document.blocks.some((block) => block.kind === 'table')) + assert.equal(document.assets.length, 1) + assert.equal(document.assets[0].mediaType, 'image/png') + assert.ok(Buffer.isBuffer(document.assets[0].data)) + assert.ok(document.assets[0].data.length > 0) +}) + +test('chm SpanInfo streams Huffman frames across dictionary resets', async () => { + const bytes = await readFile(fixture('chm/handmade-huffman-spaninfo.chm')) + assert.equal((await toMarkdownBytes(bytes)).trim(), 'A'.repeat(65536) + 'B'.repeat(65536)) +}) + test('format detection reads content, extension, and path', async () => { assert.equal(formatFromBytes(await readFile(RICH)), 'docx') // CSV carries no signature: only the extension names it. @@ -69,6 +96,9 @@ test('format detection reads content, extension, and path', async () => { assert.equal(formatFromExtension('xls'), 'xlsx') assert.equal(formatFromPath('/tmp/report.odt'), 'odt') assert.equal(formatFromPath('/tmp/report.unknown'), null) + assert.equal(formatFromBytes(await readFile(CHM)), 'chm') + assert.equal(formatFromExtension('.CHM'), 'chm') + assert.equal(formatFromPath('/tmp/help.chm'), 'chm') }) // `code` is what callers branch on, so every kind of failure is pinned here. @@ -185,6 +215,24 @@ test('cli reads stdin with an explicit format', async () => { assert.match(stdout, /\| --- \|/) }) +test('cli converts chm files and stdin with detected or explicit format', async () => { + const bytes = await readFile(CHM) + const expected = await toMarkdownBytes(bytes) + for (const args of [[CHM], [CHM, '--format', 'chm']]) { + const { code, stdout, stderr } = await runCli(args) + assert.equal(code, undefined) + assert.equal(stdout, expected) + assert.equal(stderr, '') + } + for (const args of [['-'], ['-', '--format', 'chm']]) { + const child = promisify(execFile)(process.execPath, [CLI, ...args]) + child.child.stdin.end(bytes) + const { stdout, stderr } = await child + assert.equal(stdout, expected) + assert.equal(stderr, '') + } +}) + test('cli exits 1 when the document cannot be converted', async () => { const { code, stderr } = await runCli(['no-such-file.docx']) assert.equal(code, 1) diff --git a/python/README.md b/python/README.md index 9d038bed..e8880475 100644 --- a/python/README.md +++ b/python/README.md @@ -3,7 +3,7 @@ [![PyPI](https://img.shields.io/pypi/v/firecrawl-anydoc.svg)](https://pypi.org/project/firecrawl-anydoc/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/firecrawl/anydoc/blob/main/LICENSE) -Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF files into clean GitHub-Flavored Markdown. Python bindings for the [anydoc](https://github.com/firecrawl/anydoc) Rust crate, built by [Firecrawl](https://firecrawl.dev). Also available as a hosted API through [Firecrawl Parse](https://firecrawl.dev/parse), which adds our OCR models for the scanned pages anydoc can't read on its own. +Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CHM, CSV, and PDF files into clean GitHub-Flavored Markdown. Python bindings for the [anydoc](https://github.com/firecrawl/anydoc) Rust crate, built by [Firecrawl](https://firecrawl.dev). Also available as a hosted API through [Firecrawl Parse](https://firecrawl.dev/parse), which adds our OCR models for the scanned pages anydoc can't read on its own. Every format parses into one shared document model and renders through a single Markdown serializer, so headings, tables, lists, and footnotes come out the same no matter which format goes in. Conversion releases the GIL, so other threads keep running. Type stubs ship with the package. @@ -23,6 +23,7 @@ The package installs as `firecrawl-anydoc` and imports as `anydoc`. | OpenDocument | `.odt`, `.ods`, `.odp` | | Rich Text Format | `.rtf` | | EPUB | `.epub` | +| Compiled HTML Help | `.chm` | | CSV | `.csv` | | PDF | `.pdf` | @@ -80,7 +81,7 @@ Every conversion failure subclasses `anydoc.ConvertError`, so catching that hand ## Format detection -The format is read from the file content, using the marker its specification designates: the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype and content types. CSV has no such marker, so detection returns `None` for it and the extension, or an explicit format, names it instead. +The format is read from the file content, using the marker its specification designates: the CHM ITSF header, the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype and content types. CSV has no such marker, so detection returns `None` for it and the extension, or an explicit format, names it instead. ```python anydoc.format_from_bytes(data) # 'docx', or None when nothing matches diff --git a/python/anydoc/__init__.py b/python/anydoc/__init__.py index c1ba6560..91eff90f 100644 --- a/python/anydoc/__init__.py +++ b/python/anydoc/__init__.py @@ -39,7 +39,7 @@ from anydoc._anydoc import to_markdown_bytes as _to_markdown_bytes Format = Literal[ - "doc", "docx", "odt", "pdf", "ppt", "pptx", "rtf", "epub", "xlsx", "ods", "odp", "csv" + "doc", "docx", "odt", "pdf", "ppt", "pptx", "rtf", "epub", "chm", "xlsx", "ods", "odp", "csv" ] """Input format, named after the extension that identifies it. Container variants that share a parser (`.docm`, `.xlsm`, `.ppsx`, ...) map onto these diff --git a/python/anydoc/_anydoc.pyi b/python/anydoc/_anydoc.pyi index ca45a408..0cda12c5 100644 --- a/python/anydoc/_anydoc.pyi +++ b/python/anydoc/_anydoc.pyi @@ -4,7 +4,7 @@ import os from typing import Literal, final Format = Literal[ - "doc", "docx", "odt", "pdf", "ppt", "pptx", "rtf", "epub", "xlsx", "ods", "odp", "csv" + "doc", "docx", "odt", "pdf", "ppt", "pptx", "rtf", "epub", "chm", "xlsx", "ods", "odp", "csv" ] class ConvertError(Exception): @@ -50,9 +50,9 @@ class MissingPartError(ConvertError): def format_from_bytes(data: bytes | bytearray) -> Format | None: """Detect the format from the content itself: the signature and identity - each container specification designates (PDF header, RTF open group, OLE - stream names, ZIP package mimetype/content types). Plain-text formats - (CSV) carry no signature and return `None`; so does anything + each container specification designates (CHM ITSF header, PDF header, RTF + open group, OLE stream names, ZIP package mimetype/content types). + Plain-text formats (CSV) carry no signature and return `None`; so does anything unrecognized.""" def format_from_extension(extension: str) -> Format | None: diff --git a/python/pyproject.toml b/python/pyproject.toml index 81885146..9437e8ec 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "maturin" # PyPI is held by an unrelated package) and imports as `anydoc`. [project] name = "firecrawl-anydoc" -description = "Convert documents (doc, docx, odt, rtf, epub, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown" +description = "Convert documents (doc, docx, odt, rtf, epub, chm, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown" readme = "README.md" license = "MIT" requires-python = ">=3.10" diff --git a/python/src/lib.rs b/python/src/lib.rs index a38a09de..0a58b575 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -67,7 +67,7 @@ create_exception!( /// Format names, as the extension that identifies each format. Container /// variants that share a parser (`.docm`, `.xlsm`, `.ppsx`, ...) map onto /// these via `format_from_bytes` or `format_from_extension`. -const FORMATS: [(&str, anydoc::Format); 12] = [ +const FORMATS: [(&str, anydoc::Format); 13] = [ ("doc", anydoc::Format::Doc), ("docx", anydoc::Format::Docx), ("odt", anydoc::Format::Odt), @@ -76,6 +76,7 @@ const FORMATS: [(&str, anydoc::Format); 12] = [ ("pptx", anydoc::Format::Pptx), ("rtf", anydoc::Format::Rtf), ("epub", anydoc::Format::Epub), + ("chm", anydoc::Format::Chm), ("xlsx", anydoc::Format::Excel), ("ods", anydoc::Format::Ods), ("odp", anydoc::Format::Odp), @@ -139,9 +140,10 @@ fn convert_error(py: Python<'_>, error: anydoc::ConvertError) -> PyErr { } /// Detect the format from the content itself: the signature and identity each -/// container specification designates (PDF header, RTF open group, OLE stream -/// names, ZIP package mimetype/content types). Plain-text formats (CSV) carry -/// no signature and return `None`; so does anything unrecognized. +/// container specification designates (CHM ITSF header, PDF header, RTF open +/// group, OLE stream names, ZIP package mimetype/content types). Plain-text +/// formats (CSV) carry no signature and return `None`; so does anything +/// unrecognized. #[pyfunction] fn format_from_bytes(data: Vec) -> Option<&'static str> { anydoc::Format::from_bytes(&data).map(format_name) diff --git a/python/tests/test_anydoc.py b/python/tests/test_anydoc.py index 8cff940e..b8863fd2 100644 --- a/python/tests/test_anydoc.py +++ b/python/tests/test_anydoc.py @@ -20,6 +20,7 @@ ENCRYPTED = FIXTURES / "malformed" / "encrypted--errors.odt" ZIPBOMB = FIXTURES / "abuse" / "zipbomb--errors.docx" MIXED = FIXTURES / "pdf" / "handmade-mixed.pdf" +CHM = FIXTURES / "chm" / "handmade-book.chm" HOSTED_MARKDOWN = "# Read by the hosted parser\n" @@ -93,6 +94,31 @@ def test_to_document_carries_embedded_assets_as_bytes(self): self.assertGreater(len(image.data), 0) self.assertEqual(image.id, document.assets.index(image)) + def test_chm_converts_through_the_file_bytes_and_document_apis(self): + data = CHM.read_bytes() + markdown = anydoc.to_markdown_bytes(data) + self.assertEqual(anydoc.to_markdown(CHM), markdown) + self.assertEqual(anydoc.to_markdown_bytes(data, "chm"), markdown) + for title in ("使用指南", "命令参考", "补充说明"): + self.assertIn(title, markdown) + self.assertIn("配置路由器", markdown) + self.assertIn("| 命令 | 说明 |", markdown) + self.assertIn("show version", markdown) + self.assertRegex(markdown, r"\[命令语法\]\(#[^)]+\)") + + document = anydoc.to_document(data, "chm") + self.assertEqual(len(anydoc.to_document(data).blocks), len(document.blocks)) + self.assertEqual(sum(block.kind == "heading" and block.level == 1 for block in document.blocks), 3) + self.assertTrue(any(block.kind == "table" for block in document.blocks)) + self.assertEqual(len(document.assets), 1) + self.assertEqual(document.assets[0].media_type, "image/png") + self.assertIsInstance(document.assets[0].data, bytes) + self.assertGreater(len(document.assets[0].data), 0) + + def test_chm_spaninfo_streams_huffman_frames_across_dictionary_resets(self): + data = CHM.with_name("handmade-huffman-spaninfo.chm").read_bytes() + self.assertEqual(anydoc.to_markdown_bytes(data).strip(), "A" * 65536 + "B" * 65536) + def test_format_detection_reads_content_extension_and_path(self): self.assertEqual(anydoc.format_from_bytes(RICH.read_bytes()), "docx") # CSV carries no signature: only the extension names it. @@ -101,6 +127,9 @@ def test_format_detection_reads_content_extension_and_path(self): self.assertEqual(anydoc.format_from_extension("xls"), "xlsx") self.assertEqual(anydoc.format_from_path("report.odt"), "odt") self.assertIsNone(anydoc.format_from_path("report.unknown")) + self.assertEqual(anydoc.format_from_bytes(CHM.read_bytes()), "chm") + self.assertEqual(anydoc.format_from_extension(".CHM"), "chm") + self.assertEqual(anydoc.format_from_path("help.chm"), "chm") def test_conversion_errors_raise_the_subclass_that_names_the_failure(self): with self.assertRaises(anydoc.MalformedError) as caught: diff --git a/skills/convert-documents-to-markdown/SKILL.md b/skills/convert-documents-to-markdown/SKILL.md index 5ee84694..f7a7c1b7 100644 --- a/skills/convert-documents-to-markdown/SKILL.md +++ b/skills/convert-documents-to-markdown/SKILL.md @@ -1,6 +1,6 @@ --- name: convert-documents-to-markdown -description: Convert Word (.doc, .docx), PowerPoint (.ppt, .pptx), Excel (.xls, .xlsx), OpenDocument (.odt, .ods, .odp), RTF, EPUB, CSV, and PDF files to GitHub-Flavored Markdown. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. +description: Convert Word (.doc, .docx), PowerPoint (.ppt, .pptx), Excel (.xls, .xlsx), OpenDocument (.odt, .ods, .odp), RTF, EPUB, CHM, CSV, and PDF files to GitHub-Flavored Markdown. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. license: MIT metadata: author: firecrawl @@ -18,7 +18,7 @@ npx -y @firecrawl/anydoc - --format csv < f # read stdin Rules: -1. Supported inputs: `.doc`, `.docx`, `.docm`, `.odt`, `.rtf`, `.epub`, `.pdf`, `.ppt`, `.pps`, `.pot`, `.pptx`, `.pptm`, `.ppsx`, `.ppsm`, `.odp`, `.xls`, `.xlsx`, `.xlsm`, `.xlsb`, `.ods`, `.csv`. +1. Supported inputs: `.doc`, `.docx`, `.docm`, `.odt`, `.rtf`, `.epub`, `.chm`, `.pdf`, `.ppt`, `.pps`, `.pot`, `.pptx`, `.pptm`, `.ppsx`, `.ppsm`, `.odp`, `.xls`, `.xlsx`, `.xlsm`, `.xlsb`, `.ods`, `.csv`. 2. The format is detected from the file content. Pass `--format ` only when detection cannot work: CSV from stdin, or a missing or wrong extension. 3. Exit codes: 0 success, 1 the document could not be converted, 2 usage error, 3 pages of a PDF need OCR. Failures print one `anydoc: ` line to stderr. The CLI never prompts. 4. For a large document, write to a file with `-o` and read the parts you need instead of streaming everything into context. diff --git a/src/formats/chm/archive.rs b/src/formats/chm/archive.rs new file mode 100644 index 00000000..a2f94013 --- /dev/null +++ b/src/formats/chm/archive.rs @@ -0,0 +1,1004 @@ +//! In-memory access to CHM archive entries. + +use super::lzx::{Lzxd, WindowSize}; +use crate::error::ConvertError; +use crate::package::limits; +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, Range}; +use std::rc::Rc; + +const CONTROL: &str = "::DataSpace/Storage/MSCompressed/ControlData"; +const CONTENT: &str = "::DataSpace/Storage/MSCompressed/Content"; +const SPAN_INFO: &str = "::DataSpace/Storage/MSCompressed/SpanInfo"; +const RESET_TABLE: &str = "::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable"; + +struct Entry { + name: String, + space: u64, + start: u64, + length: u64, +} + +/// A view that stays valid while other archive entries are read. Stored data +/// borrows the input; solid entries share the decoded allocation without copies. +#[derive(Clone)] +pub(super) enum Part<'a> { + Stored(&'a [u8]), + Compressed { section: Rc>, range: Range }, +} + +impl Deref for Part<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + match self { + Self::Stored(bytes) => bytes, + Self::Compressed { section, range } => §ion[range.clone()], + } + } +} + +pub(super) struct Archive<'a> { + bytes: &'a [u8], + data_offset: u64, + lcid: u32, + entries: Vec, + exact: HashMap, + folded: HashMap, + section: Option>>, + section_failed: bool, + cache: HashMap>, + total_read: u64, +} + +impl<'a> Archive<'a> { + pub(super) fn open(bytes: &'a [u8]) -> Result { + if bytes.get(..4) != Some(b"ITSF") { + return Err(bad("missing ITSF signature")); + } + let version = u32_at(bytes, 4)?; + let header_len = match version { + 2 => 0x58, + 3 => 0x60, + _ => return Err(bad("unsupported ITSF version")), + }; + if u32_at(bytes, 8)? < header_len || bytes.len() < header_len as usize { + return Err(bad("truncated ITSF header")); + } + let lcid = u32_at(bytes, 0x14)?; + let dir_offset = u64_at(bytes, 0x48)?; + let dir_length = u64_at(bytes, 0x50)?; + let data_offset = if version == 3 { + u64_at(bytes, 0x58)? + } else { + dir_offset.checked_add(dir_length).ok_or_else(|| bad("directory offset overflows"))? + }; + let directory = range(bytes, dir_offset, dir_length)?; + if directory.get(..4) != Some(b"ITSP") + || u32_at(directory, 4)? != 1 + || u32_at(directory, 8)? != 0x54 + { + return Err(bad("invalid ITSP directory header")); + } + let block_len = u32_at(directory, 0x10)? as usize; + let block_count = u32_at(directory, 0x2c)? as usize; + if !(0x14..=1024 * 1024).contains(&block_len) + || block_count > directory.len().saturating_sub(0x54) / block_len + { + return Err(bad("invalid directory block bounds")); + } + if block_count > limits::MAX_ENTRY_COUNT { + return Err(limit("max_entry_count", "too many CHM directory blocks")); + } + let mut block = u32_at(directory, 0x20)? as i32; + if block < -1 { + return Err(bad("invalid PMGL directory head")); + } + let mut visited = HashSet::new(); + let mut entries = Vec::new(); + let mut entry_count = 0; + while block >= 0 { + let index = block as usize; + if index >= block_count || !visited.insert(index) { + return Err(bad("invalid or cyclic PMGL directory chain")); + } + let offset = 0x54 + index * block_len; + let leaf = &directory[offset..offset + block_len]; + if leaf.get(..4) != Some(b"PMGL") { + return Err(bad("invalid PMGL directory block")); + } + let end = block_len + .checked_sub(u32_at(leaf, 4)? as usize) + .filter(|&end| end >= 0x14) + .ok_or_else(|| bad("invalid PMGL free-space length"))?; + let mut pos = 0x14; + while pos < end { + entry_count += 1; + if entry_count > limits::MAX_ENTRY_COUNT { + return Err(limit("max_entry_count", "too many CHM directory entries")); + } + let used = &leaf[..end]; + let name_len = usize::try_from(cword(used, &mut pos)?) + .map_err(|_| bad("entry path length overflows"))?; + let name_end = + pos.checked_add(name_len).ok_or_else(|| bad("entry path overflows"))?; + let name = std::str::from_utf8( + used.get(pos..name_end).ok_or_else(|| bad("truncated entry path"))?, + ) + .map_err(|_| bad("entry path is not UTF-8"))?; + pos = name_end; + let space = cword(used, &mut pos)?; + let start = cword(used, &mut pos)?; + let length = cword(used, &mut pos)?; + if !name.ends_with('/') { + entries.push(Entry { + name: name.trim_start_matches('/').to_string(), + space, + start, + length, + }); + } + } + block = u32_at(leaf, 0x10)? as i32; + if block < -1 { + return Err(bad("invalid PMGL directory successor")); + } + } + let mut exact = HashMap::new(); + let mut folded = HashMap::new(); + for (index, entry) in entries.iter().enumerate() { + exact.entry(entry.name.clone()).or_insert(index); + folded.entry(entry.name.to_ascii_lowercase()).or_insert(index); + } + Ok(Self { + bytes, + data_offset, + lcid, + entries, + exact, + folded, + section: None, + section_failed: false, + cache: HashMap::new(), + total_read: 0, + }) + } + + pub(super) fn entries(&self) -> impl Iterator { + self.entries.iter().map(|entry| entry.name.as_str()) + } + + pub(super) fn part(&mut self, name: &str) -> Result>, ConvertError> { + let Some(index) = self.index(name) else { return Ok(None) }; + if let Some(bytes) = self.cache.get(&index) { + return Ok(Some(bytes.clone())); + } + let entry = &self.entries[index]; + if entry.length > limits::MAX_ENTRY_BYTES { + return Err(limit( + "max_entry_bytes", + format!("{} declares {} bytes", entry.name, entry.length), + )); + } + let (space, start, length) = (entry.space, entry.start, entry.length); + if space == 1 && self.section.is_none() { + if self.section_failed { + return Err(bad("unreadable LZX data space")); + } + if let Err(error) = self.decode_section() { + // Every topic shares the stream. Do not replay a corrupt + // stream for each remaining topic in a large directory. + self.section_failed = !error.is_fatal(); + return Err(error); + } + } + let bytes = match space { + 0 => { + self.check_budget(length)?; + let bytes = self.stored(&self.entries[index])?; + self.total_read += length; + Part::Stored(bytes) + } + 1 => { + let section = self.section.as_ref().unwrap(); + range(section, start, length)?; + // The range check above also validates both usize conversions. + let range = start as usize..(start + length) as usize; + Part::Compressed { section: Rc::clone(section), range } + } + _ => return Err(bad("unsupported CHM data space")), + }; + self.cache.insert(index, bytes.clone()); + Ok(Some(bytes)) + } + + pub(super) fn lcid(&self) -> u32 { + self.lcid + } + + fn index(&self, name: &str) -> Option { + let name = name.trim_start_matches('/'); + self.exact.get(name).or_else(|| self.folded.get(&name.to_ascii_lowercase())).copied() + } + + fn stored(&self, entry: &Entry) -> Result<&'a [u8], ConvertError> { + let start = self + .data_offset + .checked_add(entry.start) + .ok_or_else(|| bad("entry offset overflows"))?; + range(self.bytes, start, entry.length) + } + + fn metadata(&self, name: &str) -> Result<&'a [u8], ConvertError> { + let index = + self.index(name).ok_or_else(|| ConvertError::MissingPart { part: name.to_string() })?; + let entry = &self.entries[index]; + if entry.space != 0 { + return Err(bad("compressed LZX control metadata")); + } + if entry.length > limits::MAX_ENTRY_BYTES { + return Err(limit("max_entry_bytes", format!("{name} is too large"))); + } + self.stored(entry) + } + + fn check_budget(&self, length: u64) -> Result<(), ConvertError> { + if length > limits::MAX_TOTAL_BYTES.saturating_sub(self.total_read) { + return Err(limit( + "max_total_bytes", + "CHM decompressed data exceeds the archive budget", + )); + } + Ok(()) + } + + /// Decode the solid stream once, in physical order. TOC order can jump + /// arbitrarily between topics; replaying LZX windows per topic would let + /// a small archive amplify CPU work. Charge the shared section once against + /// the same fixed byte budget as independently read stored entries. + fn decode_section(&mut self) -> Result<(), ConvertError> { + let control = self.metadata(CONTROL)?; + if control.get(4..8) != Some(b"LZXC") { + return Err(bad("invalid LZXC control signature")); + } + let unit = match u32_at(control, 8)? { + 1 => 1u64, + 2 => 32768, + _ => return Err(bad("unsupported LZXC control version")), + }; + let reset_interval = u64::from(u32_at(control, 12)?) * unit; + let window_bytes = u64::from(u32_at(control, 16)?) * unit; + let window = match window_bytes { + 32768 => WindowSize::KB32, + 65536 => WindowSize::KB64, + 131072 => WindowSize::KB128, + 262144 => WindowSize::KB256, + 524288 => WindowSize::KB512, + 1048576 => WindowSize::MB1, + 2097152 => WindowSize::MB2, + _ => return Err(bad("invalid CHM LZX window size")), + }; + if reset_interval == 0 || !reset_interval.is_multiple_of(32768) { + return Err(bad("invalid LZX reset interval")); + } + // ControlData's reset interval is expressed in bytes (version 1) or + // 32 KiB units (version 2), independently of the dictionary size. The + // following unknown field does not alter the decoder's reset timing. + let reset_frames = reset_interval / 32768; + let content = self.metadata(CONTENT)?; + let (length, content, offsets) = if self.index(RESET_TABLE).is_some() { + let table = self.metadata(RESET_TABLE)?; + if u32_at(table, 0)? != 2 || u64_at(table, 32)? != 32768 { + return Err(bad("invalid LZX reset table")); + } + let entry_width = match u32_at(table, 8)? { + width @ (4 | 8) => u64::from(width), + _ => return Err(bad("invalid LZX reset table entry size")), + }; + let count = u64::from(u32_at(table, 4)?); + let table_offset = u64::from(u32_at(table, 12)?); + let length = u64_at(table, 16)?; + self.check_budget(length)?; + if count != length.div_ceil(32768) || table_offset < 40 { + return Err(bad("inconsistent LZX reset table length")); + } + let offsets = range(table, table_offset, count * entry_width)?; + let content = range(content, 0, u64_at(table, 24)?)?; + (length, content, Some((offsets, entry_width))) + } else { + let span = self.metadata(SPAN_INFO)?; + if span.len() != 8 { + return Err(bad("invalid LZX SpanInfo length")); + } + let length = u64_at(span, 0)?; + if length == 0 { + return Err(bad("empty LZX SpanInfo section")); + } + self.check_budget(length)?; + (length, content, None) + }; + let count = length.div_ceil(32768); + let offset_at = |frame| { + let (offsets, width) = offsets.unwrap(); + let offset = (frame * width) as usize; + if width == 4 { + u32_at(offsets, offset).map(u64::from) + } else { + u64_at(offsets, offset) + } + }; + if offsets.is_some() && count > 0 && offset_at(0)? != 0 { + return Err(bad("LZX stream does not start at offset zero")); + } + let mut decoder = Lzxd::new(window); + let mut section = Vec::with_capacity( + usize::try_from(length).map_err(|_| bad("LZX section length overflows"))?, + ); + let mut next_offset = 0; + for frame in 0..count { + if frame.is_multiple_of(reset_frames) { + decoder.reset(); + } + let (start, end) = if offsets.is_some() { + ( + offset_at(frame)?, + if frame + 1 == count { content.len() as u64 } else { offset_at(frame + 1)? }, + ) + } else { + (next_offset, content.len() as u64) + }; + let chunk = range( + content, + start, + end.checked_sub(start).ok_or_else(|| bad("LZX offsets are not increasing"))?, + )?; + let output_len = (length - frame * 32768).min(32768) as usize; + // The decoder has assertions for malformed bitstreams. Keep an + // invalid third-party compression stream a typed conversion error. + let consumed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + decoder.decompress_next_stream(chunk, output_len).map(|(bytes, consumed)| { + section.extend_from_slice(bytes); + consumed + }) + })) + .map_err(|_| bad("invalid LZX compressed block"))? + .map_err(|error| bad(format!("corrupt LZX compressed block: {error}")))?; + if consumed == 0 { + return Err(bad("LZX frame did not consume input")); + } + next_offset = start + consumed as u64; + } + self.total_read += length; + self.section = Some(Rc::new(section)); + Ok(()) + } +} + +fn bad(detail: impl Into) -> ConvertError { + ConvertError::malformed(format!("CHM: {}", detail.into())) +} + +fn limit(name: &'static str, detail: impl Into) -> ConvertError { + ConvertError::ResourceLimit { limit: name, detail: detail.into() } +} + +fn range(bytes: &[u8], start: u64, length: u64) -> Result<&[u8], ConvertError> { + let end = start.checked_add(length).ok_or_else(|| bad("byte range overflows"))?; + let start = usize::try_from(start).map_err(|_| bad("byte offset overflows"))?; + let end = usize::try_from(end).map_err(|_| bad("byte offset overflows"))?; + bytes.get(start..end).ok_or_else(|| bad("byte range runs past the archive")) +} + +fn u32_at(bytes: &[u8], offset: usize) -> Result { + let value = bytes + .get(offset..offset.checked_add(4).ok_or_else(|| bad("offset overflows"))?) + .ok_or_else(|| bad("truncated binary field"))?; + Ok(u32::from_le_bytes(value.try_into().unwrap())) +} + +fn u64_at(bytes: &[u8], offset: usize) -> Result { + let value = bytes + .get(offset..offset.checked_add(8).ok_or_else(|| bad("offset overflows"))?) + .ok_or_else(|| bad("truncated binary field"))?; + Ok(u64::from_le_bytes(value.try_into().unwrap())) +} + +fn cword(bytes: &[u8], pos: &mut usize) -> Result { + let mut value = 0u64; + loop { + let byte = *bytes.get(*pos).ok_or_else(|| bad("truncated directory integer"))?; + *pos += 1; + if value > u64::MAX >> 7 { + return Err(bad("directory integer overflows")); + } + value = (value << 7) | u64::from(byte & 0x7f); + if byte < 0x80 { + return Ok(value); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::package::limits; + + type Entry<'a> = (&'a str, u64, u64, u64); + + fn put32(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + + fn put64(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + + fn cword(value: u64, bytes: &mut Vec) { + let mut digits = vec![(value & 0x7f) as u8]; + let mut value = value >> 7; + while value != 0 { + digits.push((value & 0x7f) as u8 | 0x80); + value >>= 7; + } + bytes.extend(digits.into_iter().rev()); + } + + /// A self-contained ITSF/ITSP/PMGL container, independent of an external compiler. + fn container(entries: &[Entry<'_>], data: &[u8]) -> Vec { + let mut directory = Vec::new(); + for &(name, space, start, length) in entries { + cword(name.len() as u64, &mut directory); + directory.extend_from_slice(name.as_bytes()); + for value in [space, start, length] { + cword(value, &mut directory); + } + } + let block_len = (directory.len() + 0x16).max(4096).next_power_of_two(); + let dir_offset = 0x60; + let block_offset = dir_offset + 0x54; + let data_offset = block_offset + block_len; + let mut bytes = vec![0u8; data_offset]; + bytes[..4].copy_from_slice(b"ITSF"); + put32(&mut bytes, 4, 3); + put32(&mut bytes, 8, 0x60); + put32(&mut bytes, 0x14, 0x0804); + put64(&mut bytes, 0x48, dir_offset as u64); + put64(&mut bytes, 0x50, (0x54 + block_len) as u64); + put64(&mut bytes, 0x58, data_offset as u64); + bytes[dir_offset..dir_offset + 4].copy_from_slice(b"ITSP"); + put32(&mut bytes, dir_offset + 4, 1); + put32(&mut bytes, dir_offset + 8, 0x54); + put32(&mut bytes, dir_offset + 0x10, block_len as u32); + put32(&mut bytes, dir_offset + 0x1c, u32::MAX); + put32(&mut bytes, dir_offset + 0x20, 0); + put32(&mut bytes, dir_offset + 0x24, 0); + put32(&mut bytes, dir_offset + 0x2c, 1); + bytes[block_offset..block_offset + 4].copy_from_slice(b"PMGL"); + put32(&mut bytes, block_offset + 4, (block_len - 0x14 - directory.len()) as u32); + put32(&mut bytes, block_offset + 0x0c, u32::MAX); + put32(&mut bytes, block_offset + 0x10, u32::MAX); + bytes[block_offset + 0x14..block_offset + 0x14 + directory.len()] + .copy_from_slice(&directory); + bytes.extend_from_slice(data); + bytes + } + + fn stored(parts: &[(&str, &[u8])]) -> Vec { + let mut data = Vec::new(); + let mut entries = Vec::new(); + for &(name, bytes) in parts { + entries.push((name, 0, data.len() as u64, bytes.len() as u64)); + data.extend_from_slice(bytes); + } + container(&entries, &data) + } + + fn compressed_parts(payload: &[u8], parts: &[Entry<'_>]) -> Vec { + let source = compressed(payload); + let archive = Archive::open(&source).unwrap(); + let mut entries: Vec<_> = archive + .entries + .iter() + .map(|entry| (entry.name.as_str(), entry.space, entry.start, entry.length)) + .collect(); + entries.extend_from_slice(parts); + container(&entries, &source[archive.data_offset as usize..]) + } + + fn compressed(payload: &[u8]) -> Vec { + // One LZX uncompressed block: optional E8 flag, block kind 3, 24-bit + // length, alignment, then the three repeated offsets and the raw bytes. + let bits = (3u32 << 28) | ((payload.len() as u32) << 4); + let mut stream = Vec::new(); + stream.extend_from_slice(&((bits >> 16) as u16).to_le_bytes()); + stream.extend_from_slice(&(bits as u16).to_le_bytes()); + for _ in 0..3 { + stream.extend_from_slice(&1u32.to_le_bytes()); + } + stream.extend_from_slice(payload); + if !payload.len().is_multiple_of(2) { + stream.push(0); + } + let mut control = vec![0u8; 28]; + put32(&mut control, 0, 6); + control[4..8].copy_from_slice(b"LZXC"); + for (offset, value) in [(8, 2), (12, 2), (16, 2), (20, 1)] { + put32(&mut control, offset, value); + } + let mut table = vec![0u8; 48]; + put32(&mut table, 0, 2); + put32(&mut table, 4, 1); + put32(&mut table, 8, 8); + put32(&mut table, 12, 40); + put64(&mut table, 16, payload.len() as u64); + put64(&mut table, 24, stream.len() as u64); + put64(&mut table, 32, 32768); + let content_offset = control.len() + table.len(); + let mut data = control; + data.extend_from_slice(&table); + data.extend_from_slice(&stream); + container( + &[ + ("/page.html", 1, 0, payload.len() as u64), + ("::DataSpace/Storage/MSCompressed/ControlData", 0, 0, 28), + ( + "::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable", + 0, + 28, + 48, + ), + ( + "::DataSpace/Storage/MSCompressed/Content", + 0, + content_offset as u64, + stream.len() as u64, + ), + ], + &data, + ) + } + + /// Write MSB-first bits as the little-endian 16-bit words used by LZX. + #[derive(Default)] + struct Bits(Vec); + + impl Bits { + fn push(&mut self, value: u32, count: u8) { + self.0.extend((0..count).rev().map(|shift| value & (1 << shift) != 0)); + } + + fn finish(mut self) -> Vec { + self.0.resize(self.0.len().next_multiple_of(16), false); + self.0 + .chunks(16) + .flat_map(|bits| { + bits.iter().fold(0u16, |word, &bit| (word << 1) | u16::from(bit)).to_le_bytes() + }) + .collect() + } + + fn tree_lengths(&mut self, lengths: &[u8]) { + // Complete canonical pretree: 12 codes of length 4, 8 of length 5. + for symbol in 0..20 { + self.push(if symbol < 12 { 4 } else { 5 }, 4); + } + for &length in lengths { + let delta = (17 - u32::from(length)) % 17; + if delta < 12 { + self.push(delta, 4); + } else { + self.push(24 + delta - 12, 5); + } + } + } + } + + /// Original verbatim-Huffman LZX block spanning two frames. The second + /// frame contains only matches, so decoding it depends on the first frame's + /// dictionary, main tree, and unfinished block. A second group must reset. + fn huffman_group(literal: u8) -> Vec> { + huffman_group_with_header(literal, true) + } + + fn huffman_group_with_header(literal: u8, first_chunk: bool) -> Vec> { + let mut bits = Bits::default(); + if first_chunk { + bits.push(0, 1); // No E8 translation. + } + bits.push(1, 3); // Verbatim Huffman block, not an uncompressed LZX block. + bits.push(65536, 24); + let mut main = [0u8; 512]; // 64 KiB window: 32 position slots. + main[usize::from(b'A')] = 2; + main[usize::from(b'B')] = 2; + main[261] = 2; // R0 match of length 7. + main[262] = 2; // R0 match of length 8. + bits.tree_lengths(&main[..256]); + bits.tree_lengths(&main[256..]); + bits.tree_lengths(&[0; 249]); + bits.push(u32::from(literal - b'A'), 2); + for _ in 0..4095 { + bits.push(3, 2); + } + bits.push(2, 2); + let first = bits.finish(); + let mut bits = Bits::default(); + for _ in 0..4096 { + bits.push(3, 2); + } + vec![first, bits.finish()] + } + + fn framed(chunks: &[Vec], length: u64, width: Option, window_units: u32) -> Vec { + let mut control = vec![0; 28]; + put32(&mut control, 0, 6); + control[4..8].copy_from_slice(b"LZXC"); + for (offset, value) in [(8, 2), (12, 2), (16, window_units), (20, 1)] { + put32(&mut control, offset, value); + } + let stream = chunks.concat(); + let metadata = if let Some(width) = width { + let mut table = vec![0; 40 + chunks.len() * width as usize]; + put32(&mut table, 0, 2); + put32(&mut table, 4, chunks.len() as u32); + put32(&mut table, 8, width); + put32(&mut table, 12, 40); + put64(&mut table, 16, length); + put64(&mut table, 24, stream.len() as u64); + put64(&mut table, 32, 32768); + let mut offset = 0; + for (i, chunk) in chunks.iter().enumerate() { + if width == 4 { + put32(&mut table, 40 + i * 4, offset as u32); + } else { + put64(&mut table, 40 + i * 8, offset); + } + offset += chunk.len() as u64; + } + table + } else { + length.to_le_bytes().to_vec() + }; + let mut data = control; + data.extend_from_slice(&metadata); + data.extend_from_slice(&stream); + container( + &[ + ("/page.html", 1, 0, length), + (CONTROL, 0, 0, 28), + ( + if width.is_some() { RESET_TABLE } else { SPAN_INFO }, + 0, + 28, + metadata.len() as u64, + ), + (CONTENT, 0, 28 + metadata.len() as u64, stream.len() as u64), + ], + &data, + ) + } + + #[test] + fn four_byte_reset_offsets_decode_huffman_frames_with_shared_dictionary() { + let chunks = huffman_group(b'A'); + let bytes = framed(&chunks, 65536, Some(4), 2); + let mut archive = Archive::open(&bytes).unwrap(); + assert_eq!(&*archive.part("page.html").unwrap().unwrap(), vec![b'A'; 65536]); + } + + #[test] + fn huffman_frames_reset_at_control_interval_independently_of_unknown_field() { + // A 64 KiB reset interval. The unknown + // ControlData field at offset 20 must not change reset timing. + let mut chunks = huffman_group(b'A'); + chunks.extend(huffman_group(b'B')); + let mut bytes = framed(&chunks, 131072, Some(8), 2); + let data_offset = u64_at(&bytes, 0x58).unwrap() as usize; + put32(&mut bytes, data_offset + 20, 2); + let mut archive = Archive::open(&bytes).unwrap(); + let mut expected = vec![b'A'; 65536]; + expected.extend(vec![b'B'; 65536]); + assert_eq!(&*archive.part("page.html").unwrap().unwrap(), expected); + } + + #[test] + fn spaninfo_decodes_huffman_frames_with_dictionary_and_resets() { + let mut chunks = huffman_group(b'A'); + chunks.extend(huffman_group(b'B')); + let bytes = framed(&chunks, 131072, None, 2); + let mut archive = Archive::open(&bytes).unwrap(); + let mut expected = vec![b'A'; 65536]; + expected.extend(vec![b'B'; 65536]); + assert_eq!(&*archive.part("page.html").unwrap().unwrap(), expected); + } + + fn raw_stream(payload: &[u8], e8_size: Option) -> Vec { + let mut bits = Bits::default(); + bits.push(u32::from(e8_size.is_some()), 1); + if let Some(size) = e8_size { + bits.push(size, 32); + } + bits.push(3, 3); + bits.push(payload.len() as u32, 24); + let mut stream = bits.finish(); + for _ in 0..3 { + stream.extend_from_slice(&1u32.to_le_bytes()); + } + stream.extend_from_slice(payload); + if !payload.len().is_multiple_of(2) { + stream.push(0); + } + stream + } + + #[test] + fn spaninfo_streams_unfinished_raw_blocks_and_final_odd_padding() { + let expected = (0..65537).map(|i| (i % 251) as u8).collect::>(); + let stream = raw_stream(&expected, None); + let mut bytes = framed(&[stream], expected.len() as u64, None, 2); + let data_offset = u64_at(&bytes, 0x58).unwrap() as usize; + put32(&mut bytes, data_offset + 12, 4); + let mut archive = Archive::open(&bytes).unwrap(); + assert_eq!(&*archive.part("page.html").unwrap().unwrap(), expected); + } + + #[test] + fn spaninfo_consumes_odd_block_padding_at_a_frame_boundary() { + let mut stream = raw_stream(&vec![b'A'; 32767], None); + let continuation = |payload: &[u8]| { + let mut bits = Bits::default(); + bits.push(3, 3); + bits.push(payload.len() as u32, 24); + let mut block = bits.finish(); + for _ in 0..3 { + block.extend_from_slice(&1u32.to_le_bytes()); + } + block.extend_from_slice(payload); + if !payload.len().is_multiple_of(2) { + block.push(0); + } + block + }; + stream.extend(continuation(b"B")); + stream.extend(continuation(&vec![b'C'; 32768])); + let bytes = framed(&[stream], 65536, None, 2); + let mut archive = Archive::open(&bytes).unwrap(); + let mut expected = vec![b'A'; 32767]; + expected.push(b'B'); + expected.extend(vec![b'C'; 32768]); + assert_eq!(&*archive.part("page.html").unwrap().unwrap(), expected); + } + + #[test] + fn spaninfo_preserves_e8_positions_across_frames() { + let mut payload = vec![b'x'; 65536]; + let mut expected = payload.clone(); + for position in [20, 32768 + 20] { + payload[position] = 0xe8; + payload[position + 1..position + 5].copy_from_slice(&50000i32.to_le_bytes()); + expected[position] = 0xe8; + expected[position + 1..position + 5] + .copy_from_slice(&(50000i32 - position as i32).to_le_bytes()); + } + let stream = raw_stream(&payload, Some(65536)); + let bytes = framed(&[stream], payload.len() as u64, None, 2); + let mut archive = Archive::open(&bytes).unwrap(); + assert_eq!(&*archive.part("page.html").unwrap().unwrap(), expected); + } + + #[test] + fn spaninfo_keeps_absolute_e8_positions_across_dictionary_resets() { + let mut payload = vec![b'x'; 65536]; + payload[20] = 0xe8; + payload[21..25].copy_from_slice(&50000i32.to_le_bytes()); + let stream = raw_stream(&payload, Some(131072)); + let bytes = framed(&[stream.clone(), stream], 131072, None, 2); + let mut archive = Archive::open(&bytes).unwrap(); + let topic = archive.part("page.html").unwrap().unwrap(); + assert_eq!(&topic[21..25], &49980i32.to_le_bytes()); + assert_eq!(&topic[65536 + 21..65536 + 25], &(-15556i32).to_le_bytes()); + } + + #[test] + fn spaninfo_translates_an_e8_operand_targeting_position_zero() { + let mut payload = vec![b'x'; 32]; + payload[20] = 0xe8; + payload[21..25].copy_from_slice(&0i32.to_le_bytes()); + let stream = raw_stream(&payload, Some(65536)); + let bytes = framed(&[stream], payload.len() as u64, None, 2); + let mut archive = Archive::open(&bytes).unwrap(); + let topic = archive.part("page.html").unwrap().unwrap(); + assert_eq!(&topic[21..25], &(-20i32).to_le_bytes()); + } + + #[test] + fn invalid_repeated_match_offsets_return_errors_without_unwinding() { + for offset in [0, 65536, u32::MAX] { + let mut raw = raw_stream(&vec![b'A'; 32768], None); + put32(&mut raw, 4, offset); + let following = huffman_group_with_header(b'A', false); + let result = std::panic::catch_unwind(|| { + let mut decoder = Lzxd::new(WindowSize::KB64); + decoder.decompress_next_stream(&raw, 32768).unwrap(); + decoder.decompress_next_stream(&following[0], 32768).is_err() + }); + assert!( + matches!(result, Ok(true)), + "invalid R0 {offset} must return an error without panicking" + ); + } + } + + #[test] + fn spaninfo_size_budget_and_truncation_are_enforced() { + let bytes = framed(&[], limits::MAX_TOTAL_BYTES + 1, None, 2); + let mut archive = Archive::open(&bytes).unwrap(); + assert!(matches!( + archive.decode_section(), + Err(ConvertError::ResourceLimit { limit: "max_total_bytes", .. }) + )); + let stream = raw_stream(b"truncated topic", None); + let bytes = framed(&[stream[..stream.len() - 2].to_vec()], 15, None, 2); + let mut archive = Archive::open(&bytes).unwrap(); + assert!(matches!(archive.part("page.html"), Err(ConvertError::Malformed { .. }))); + assert!(archive.section_failed); + assert!(archive.section.is_none()); + } + + #[test] + fn spaninfo_decodes_a_single_frame_without_reset_table() { + let source = compressed(b"span topic"); + let archive = Archive::open(&source).unwrap(); + let stream = archive.metadata(CONTENT).unwrap().to_vec(); + let bytes = framed(&[stream], 10, None, 2); + let mut archive = Archive::open(&bytes).unwrap(); + assert_eq!(&*archive.part("page.html").unwrap().unwrap(), b"span topic"); + } + + #[test] + fn stored_entries_are_read_from_memory_and_repeated_reads_share_bytes() { + let bytes = stored(&[("/", b""), ("/Page.HTML", b"

Page

"), ("/#SYSTEM", b"meta")]); + let mut archive = Archive::open(&bytes).unwrap(); + assert_eq!(archive.lcid(), 0x0804); + assert_eq!(archive.entries().collect::>(), ["Page.HTML", "#SYSTEM"]); + let first = archive.part("page.html").unwrap().unwrap(); + let second = archive.part("/Page.HTML").unwrap().unwrap(); + assert_eq!(&*first, b"

Page

"); + assert!(std::ptr::eq(first.as_ptr(), second.as_ptr())); + assert!(archive.part("missing.html").unwrap().is_none()); + } + + #[test] + fn stored_parts_borrow_input_and_outlive_archive_access() { + let bytes = stored(&[("/first", b"first"), ("/second", b"second")]); + let mut archive = Archive::open(&bytes).unwrap(); + let input = archive.stored(&archive.entries[0]).unwrap(); + let first = archive.part("first").unwrap().unwrap(); + assert!(std::ptr::eq(first.as_ptr(), input.as_ptr())); + let second = archive.part("second").unwrap().unwrap(); + drop(archive); + assert_eq!(&*first, b"first"); + assert_eq!(&*second, b"second"); + } + + #[test] + fn stored_parts_still_charge_the_budget_only_once() { + let bytes = stored(&[("/first", b"first"), ("/second", b"second")]); + let mut archive = Archive::open(&bytes).unwrap(); + archive.total_read = limits::MAX_TOTAL_BYTES - 5; + assert_eq!(&*archive.part("first").unwrap().unwrap(), b"first"); + assert_eq!(&*archive.part("first").unwrap().unwrap(), b"first"); + assert_eq!(archive.total_read, limits::MAX_TOTAL_BYTES); + assert!(matches!( + archive.part("second"), + Err(ConvertError::ResourceLimit { limit: "max_total_bytes", .. }) + )); + } + + #[test] + fn compressed_parts_share_one_section_and_outlive_archive_access() { + let bytes = compressed_parts(b"abcdef", &[("/middle", 1, 1, 3)]); + let mut archive = Archive::open(&bytes).unwrap(); + let whole = archive.part("page.html").unwrap().unwrap(); + assert!(std::ptr::eq(whole.as_ptr(), archive.section.as_ref().unwrap().as_ptr())); + // Later entries use the already decoded section even if its stream + // metadata becomes unreadable. They retain it after Archive is dropped. + let content_index = archive.index(CONTENT).unwrap(); + archive.entries[content_index].start = u64::MAX; + let middle = archive.part("middle").unwrap().unwrap(); + assert!(std::ptr::eq(middle.as_ptr(), whole[1..].as_ptr())); + assert_eq!(archive.total_read, 6); + drop(archive); + assert_eq!(&*whole, b"abcdef"); + assert_eq!(&*middle, b"bcd"); + } + + #[test] + fn repeated_and_overlapping_compressed_parts_do_not_charge_section_twice() { + let bytes = compressed_parts(b"abcdef", &[("/middle", 1, 1, 3), ("/alias", 1, 0, 6)]); + let mut archive = Archive::open(&bytes).unwrap(); + archive.total_read = limits::MAX_TOTAL_BYTES - 6; + for (name, expected) in [ + ("page.html", b"abcdef".as_slice()), + ("page.html", b"abcdef".as_slice()), + ("middle", b"bcd".as_slice()), + ("alias", b"abcdef".as_slice()), + ] { + assert_eq!(&*archive.part(name).unwrap().unwrap(), expected); + assert_eq!(archive.total_read, limits::MAX_TOTAL_BYTES); + } + } + + #[test] + fn exact_case_wins_and_case_insensitive_fallback_is_deterministic() { + let bytes = stored(&[("/Page.html", b"first"), ("/page.html", b"second")]); + let mut archive = Archive::open(&bytes).unwrap(); + assert_eq!(&*archive.part("page.html").unwrap().unwrap(), b"second"); + assert_eq!(&*archive.part("PAGE.HTML").unwrap().unwrap(), b"first"); + } + + #[test] + fn lzx_section_is_decoded_and_cached() { + let bytes = compressed(b"

LZX topic

"); + let mut archive = Archive::open(&bytes).unwrap(); + let first = archive.part("page.html").unwrap().unwrap(); + assert_eq!(&*first, b"

LZX topic

"); + assert!(std::ptr::eq(first.as_ptr(), archive.part("page.html").unwrap().unwrap().as_ptr())); + } + + #[test] + fn invalid_headers_and_directory_cycles_are_rejected() { + assert!(Archive::open(b"ITSF").is_err()); + let mut bytes = stored(&[("/page.html", b"page")]); + put32(&mut bytes, 0x60 + 0x54 + 0x10, 0); + assert!(Archive::open(&bytes).is_err()); + } + + #[test] + fn entry_count_is_limited_before_collecting_the_archive() { + let bytes = container(&vec![("/x", 0, 0, 0); limits::MAX_ENTRY_COUNT + 1], &[]); + assert!(matches!( + Archive::open(&bytes), + Err(ConvertError::ResourceLimit { limit: "max_entry_count", .. }) + )); + } + + #[test] + fn oversized_and_overflowing_entries_fail_before_reading() { + let bytes = container( + &[ + ("/huge.html", 0, 0, limits::MAX_ENTRY_BYTES + 1), + ("/overflow.html", 0, u64::MAX, 8), + ], + &[], + ); + let mut archive = Archive::open(&bytes).unwrap(); + assert!(matches!( + archive.part("huge.html"), + Err(ConvertError::ResourceLimit { limit: "max_entry_bytes", .. }) + )); + assert!(archive.part("overflow.html").is_err()); + } + + #[test] + fn oversized_solid_section_is_rejected_before_allocating() { + let mut bytes = compressed(b"topic"); + let data_offset = u64_at(&bytes, 0x58).unwrap() as usize; + put64(&mut bytes, data_offset + 28 + 16, limits::MAX_TOTAL_BYTES + 1); + let mut archive = Archive::open(&bytes).unwrap(); + assert!(matches!( + archive.part("page.html"), + Err(ConvertError::ResourceLimit { limit: "max_total_bytes", .. }) + )); + } + + #[test] + fn corrupt_solid_stream_is_not_replayed_for_later_topics() { + let mut bytes = compressed(b"topic"); + bytes.pop(); + let mut archive = Archive::open(&bytes).unwrap(); + assert!(archive.part("page.html").is_err()); + assert!(archive.section_failed); + assert!(archive.part("page.html").is_err()); + assert!(archive.section.is_none()); + } +} diff --git a/src/formats/chm/html.rs b/src/formats/chm/html.rs new file mode 100644 index 00000000..45bd6942 --- /dev/null +++ b/src/formats/chm/html.rs @@ -0,0 +1,913 @@ +//! Legacy HTML decoding and adaptation to the shared element tree. + +use crate::error::ConvertError; +use crate::package::limits; +use crate::package::xml::{Attr, Element, Node}; +use encoding_rs::Encoding; +use html5ever::interface::tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink}; +use html5ever::tendril::{StrTendril, TendrilSink}; +use html5ever::tokenizer::states::RawKind; +use html5ever::tokenizer::{BufferQueue, StartTag, Token, TokenSink, TokenSinkResult, Tokenizer}; +use html5ever::{Attribute, QualName, parse_document}; +use std::borrow::Cow; +use std::cell::{Cell, RefCell}; +use std::rc::{Rc, Weak}; + +pub(super) fn parse(bytes: &[u8], fallback: &'static Encoding) -> Result { + parse_with_encoding(bytes, fallback).map(|(tree, _)| tree) +} + +pub(super) fn parse_with_encoding( + bytes: &[u8], + fallback: &'static Encoding, +) -> Result<(Element, &'static Encoding), ConvertError> { + if bytes.len() as u64 > limits::MAX_ENTRY_BYTES { + return Err(limit_error("max_entry_bytes")); + } + let encoding = encoding(bytes, fallback); + let text = decode_as(bytes, encoding); + let sink = Sink::new(); + let failure = Rc::clone(&sink.failure); + let mut parser = parse_document(sink, Default::default()); + // Check the sink between bounded chunks: malformed HTML cannot keep + // building a DOM after the first exhausted resource budget. + let mut start = 0; + while start < text.len() { + let mut end = (start + 8192).min(text.len()); + while !text.is_char_boundary(end) { + end -= 1; + } + parser.process(text[start..end].into()); + if let Some(limit) = failure.get() { + return Err(limit_error(limit)); + } + start = end; + } + let document = parser.finish(); + if let Some(limit) = failure.get() { + return Err(limit_error(limit)); + } + // HTML's adoption/reparenting rules can move a whole subtree. Validate + // the final depth too, before the shared recursive element walker runs. + let mut stack = vec![(Rc::clone(&document.root), 0usize)]; + while let Some((node, depth)) = stack.pop() { + let depth = depth + usize::from(matches!(node.kind, Kind::Element { .. })); + if depth > limits::MAX_XML_DEPTH { + return Err(limit_error("max_xml_depth")); + } + stack.extend(node.children().map(|child| (child, depth))); + } + Ok(( + Element { + ns: None, + local: String::new(), + attrs: Vec::new(), + children: document.root.children().filter_map(|child| adapt(&child)).collect(), + }, + encoding, + )) +} + +fn limit_error(limit: &'static str) -> ConvertError { + ConvertError::ResourceLimit { + limit, + detail: "CHM HTML exceeds the document parsing budget".into(), + } +} + +/// HTML metadata is ASCII even when the body is not. Sniff it before +/// decoding; unlike XML, old HTML commonly declares its charset in a meta tag. +pub(super) fn decode(bytes: &[u8], fallback: &'static Encoding) -> String { + decode_as(bytes, encoding(bytes, fallback)) +} + +pub(super) fn decode_stylesheet(bytes: &[u8], fallback: &'static Encoding) -> String { + let encoding = Encoding::for_bom(bytes) + .map(|(enc, _)| enc) + .or_else(|| { + stylesheet_charset(bytes).map(|(encoding, _)| { + // An ASCII @charset declaration cannot describe UTF-16 bytes. + if encoding == encoding_rs::UTF_16LE || encoding == encoding_rs::UTF_16BE { + encoding_rs::UTF_8 + } else { + encoding + } + }) + }) + .unwrap_or(fallback); + let mut text = decode_as(bytes, encoding); + // The shared CSS subset starts with selectors, so remove the consumed + // declaration before handing it the first rule. Inspect decoded text + // too: a BOM may have taken precedence over the declared encoding. + if let Some((_, end)) = stylesheet_charset(text.as_bytes()) { + text.drain(..end); + } + text +} + +fn stylesheet_charset(bytes: &[u8]) -> Option<(&'static Encoding, usize)> { + // CSS's byte-level encoding declaration must be the first rule, + // with this exact syntax. HTML meta tags, comments, and strings + // in a stylesheet do not declare its encoding. + const PREFIX: &[u8] = b"@charset \""; + let declaration = bytes[..bytes.len().min(1024)].strip_prefix(PREFIX)?; + let end = declaration.iter().position(|&byte| byte == b'"')?; + if declaration.get(end + 1) != Some(&b';') { + return None; + } + let encoding = Encoding::for_label(&declaration[..end])?; + Some((encoding, PREFIX.len() + end + 2)) +} + +pub(super) fn encoding(bytes: &[u8], fallback: &'static Encoding) -> &'static Encoding { + // ASCII and some legacy byte sequences are also valid UTF-8. Keep the + // book's encoding unless the page explicitly declares another one: it + // also governs percent-encoded URLs in otherwise ASCII-only pages. + Encoding::for_bom(bytes) + .map(|(enc, _)| enc) + .or_else(|| sniff_encoding(bytes)) + .unwrap_or(fallback) +} + +fn decode_as(bytes: &[u8], encoding: &'static Encoding) -> String { + let (text, _, errors) = encoding.decode(bytes); + if errors { + log::warn!("replaced undecodable characters in CHM HTML or stylesheet"); + } + text.into_owned() +} + +fn sniff_encoding(bytes: &[u8]) -> Option<&'static Encoding> { + let head: String = bytes[..bytes.len().min(4096)] + .iter() + .map(|b| if b.is_ascii() { *b as char } else { ' ' }) + .collect(); + // Retain support for XHTML's XML declaration, restricted to the prolog. + if let Some(prolog) = head.trim_start().strip_prefix("") + && let Some(pos) = prolog.find("encoding") + && let Some(value) = prolog[pos + "encoding".len()..].trim_start().strip_prefix('=') + { + let value = value.trim_start().trim_start_matches(['\'', '"']); + let end = value.find(['\'', '"', ' ', '\t', '\r', '\n']).unwrap_or(value.len()); + if let Some(encoding) = Encoding::for_label(&value.as_bytes()[..end]) { + return Some(encoding); + } + } + // A bounded tokenizer pass recognizes real attributes and ignores + // comments/raw text without building a second DOM. + let tokenizer = Tokenizer::new(CharsetSniff(Cell::new(None)), Default::default()); + let input = BufferQueue::default(); + input.push_back(head.into()); + let _ = tokenizer.feed(&input); + tokenizer.end(); + tokenizer.sink.0.get() +} + +struct CharsetSniff(Cell>); + +impl TokenSink for CharsetSniff { + type Handle = (); + + fn process_token(&self, token: Token, _: u64) -> TokenSinkResult<()> { + let Token::TagToken(tag) = token else { return TokenSinkResult::Continue }; + if tag.kind != StartTag { + return TokenSinkResult::Continue; + } + match tag.name.as_ref() { + "script" => return TokenSinkResult::RawData(RawKind::ScriptData), + "style" | "xmp" | "iframe" | "noembed" | "noframes" | "noscript" => { + return TokenSinkResult::RawData(RawKind::Rawtext); + } + "textarea" | "title" => return TokenSinkResult::RawData(RawKind::Rcdata), + "plaintext" => return TokenSinkResult::Plaintext, + "meta" if self.0.get().is_none() => { + let attr = |name: &str| { + tag.attrs.iter().find(|a| &*a.name.local == name).map(|a| &*a.value) + }; + let label = attr("charset").or_else(|| { + if !attr("http-equiv") + .is_some_and(|value| value.eq_ignore_ascii_case("content-type")) + { + return None; + } + attr("content")?.split(';').find_map(|part| { + let (name, value) = part.split_once('=')?; + name.trim() + .eq_ignore_ascii_case("charset") + .then(|| value.trim().trim_matches(['\'', '"'])) + }) + }); + if let Some(encoding) = + label.and_then(|label| Encoding::for_label(label.as_bytes())) + { + self.0.set(Some(encoding)); + } + } + _ => {} + } + TokenSinkResult::Continue + } +} + +type Handle = Rc; + +#[cfg(test)] +thread_local! { + // Count traversed/moved siblings, rather than relying on CI timing. + static SIBLING_STEPS: Cell = const { Cell::new(0) }; +} + +#[cfg(test)] +fn sibling_steps(count: usize) { + SIBLING_STEPS.with(|steps| steps.set(steps.get() + count)); +} + +struct DomNode { + kind: Kind, + links: RefCell, +} + +// The flat arena owns nodes. Weak sibling links allow constant-time insertion +// and removal, without recursively dropping a wide sibling chain at the end. +#[derive(Default)] +struct Links { + parent: Weak, + previous: Weak, + next: Weak, + first: Weak, + last: Weak, +} + +struct Children { + next: Option, +} + +impl Iterator for Children { + type Item = Handle; + + fn next(&mut self) -> Option { + let node = self.next.take()?; + #[cfg(test)] + sibling_steps(1); + self.next = node.links.borrow().next.upgrade(); + Some(node) + } +} + +struct Dom { + root: Handle, + _nodes: Vec, +} + +enum Kind { + Document, + Element { + name: QualName, + attrs: RefCell>, + template: Option, + mathml_point: bool, + }, + Text(RefCell), + Ignored, +} + +impl DomNode { + fn new(kind: Kind) -> Handle { + Rc::new(Self { kind, links: RefCell::new(Links::default()) }) + } + + fn children(&self) -> Children { + Children { next: self.links.borrow().first.upgrade() } + } + + fn detach(node: &Handle) { + let mut links = node.links.borrow_mut(); + let Some(parent) = links.parent.upgrade() else { return }; + let mut parent_links = parent.links.borrow_mut(); + if let Some(previous) = links.previous.upgrade() { + previous.links.borrow_mut().next = links.next.clone(); + } else { + parent_links.first = links.next.clone(); + } + if let Some(next) = links.next.upgrade() { + next.links.borrow_mut().previous = links.previous.clone(); + } else { + parent_links.last = links.previous.clone(); + } + links.parent = Weak::new(); + links.previous = Weak::new(); + links.next = Weak::new(); + } +} + +struct Sink { + document: Handle, + nodes: Cell, + arena: RefCell>, + failure: Rc>>, +} + +impl Sink { + fn new() -> Self { + let document = DomNode::new(Kind::Document); + Self { + arena: RefCell::new(vec![Rc::clone(&document)]), + document, + nodes: Cell::new(1), + failure: Rc::new(Cell::new(None)), + } + } + + fn node(&self, kind: Kind) -> Handle { + self.nodes.set(self.nodes.get().saturating_add(1)); + if self.nodes.get() > limits::MAX_XML_NODES { + self.failure.set(Some("max_xml_nodes")); + } + let node = DomNode::new(kind); + if !matches!(node.kind, Kind::Ignored) { + self.arena.borrow_mut().push(Rc::clone(&node)); + } + node + } + + fn insert(&self, parent: &Handle, child: NodeOrText, before: Option<&Handle>) { + if self.failure.get().is_some() { + return; + } + let before = before.filter(|sibling| { + sibling.links.borrow().parent.upgrade().is_some_and(|p| Rc::ptr_eq(&p, parent)) + }); + let child = match child { + NodeOrText::AppendNode(node) => { + if before.is_some_and(|sibling| Rc::ptr_eq(sibling, &node)) { + return; + } + node + } + NodeOrText::AppendText(text) => { + let previous = match before { + Some(sibling) => sibling.links.borrow().previous.upgrade(), + None => parent.links.borrow().last.upgrade(), + }; + if let Some(previous) = previous + && let Kind::Text(contents) = &previous.kind + { + contents.borrow_mut().push_tendril(&text); + return; + } + self.node(Kind::Text(RefCell::new(text))) + } + }; + if matches!(child.kind, Kind::Ignored) { + return; + } + let mut cursor = Some(Rc::clone(parent)); + let mut depth = usize::from(matches!(child.kind, Kind::Element { .. })); + while let Some(node) = cursor { + if Rc::ptr_eq(&node, &child) { + self.failure.set(Some("max_xml_depth")); + return; + } + depth += usize::from(matches!(node.kind, Kind::Element { .. })); + if depth > limits::MAX_XML_DEPTH { + self.failure.set(Some("max_xml_depth")); + return; + } + cursor = node.links.borrow().parent.upgrade(); + } + DomNode::detach(&child); + let previous = match before { + Some(sibling) => sibling.links.borrow().previous.clone(), + None => parent.links.borrow().last.clone(), + }; + let child_weak = Rc::downgrade(&child); + if let Some(previous) = previous.upgrade() { + previous.links.borrow_mut().next = child_weak.clone(); + } else { + parent.links.borrow_mut().first = child_weak.clone(); + } + if let Some(next) = before { + next.links.borrow_mut().previous = child_weak.clone(); + } else { + parent.links.borrow_mut().last = child_weak; + } + let mut links = child.links.borrow_mut(); + links.parent = Rc::downgrade(parent); + links.previous = previous; + links.next = before.map_or_else(Weak::new, Rc::downgrade); + } +} + +impl TreeSink for Sink { + type Handle = Handle; + type Output = Dom; + type ElemName<'a> = &'a QualName; + + fn finish(self) -> Dom { + Dom { root: self.document, _nodes: self.arena.into_inner() } + } + fn parse_error(&self, _: Cow<'static, str>) {} + fn get_document(&self) -> Handle { + Rc::clone(&self.document) + } + fn same_node(&self, x: &Handle, y: &Handle) -> bool { + Rc::ptr_eq(x, y) + } + fn set_quirks_mode(&self, _: QuirksMode) {} + + fn elem_name<'a>(&'a self, target: &'a Handle) -> &'a QualName { + match &target.kind { + Kind::Element { name, .. } => name, + _ => unreachable!("HTML tree builder requested a non-element name"), + } + } + + fn create_element(&self, name: QualName, attrs: Vec, flags: ElementFlags) -> Handle { + self.node(Kind::Element { + name, + attrs: RefCell::new(attrs), + template: flags.template.then(|| self.node(Kind::Document)), + mathml_point: flags.mathml_annotation_xml_integration_point, + }) + } + + fn create_comment(&self, _: StrTendril) -> Handle { + self.node(Kind::Ignored) + } + fn create_pi(&self, _: StrTendril, _: StrTendril) -> Handle { + self.node(Kind::Ignored) + } + fn append(&self, parent: &Handle, child: NodeOrText) { + self.insert(parent, child, None); + } + fn append_before_sibling(&self, sibling: &Handle, child: NodeOrText) { + let parent = sibling.links.borrow().parent.upgrade(); + if let Some(parent) = parent { + self.insert(&parent, child, Some(sibling)); + } + } + fn append_based_on_parent_node( + &self, + element: &Handle, + previous: &Handle, + child: NodeOrText, + ) { + if element.links.borrow().parent.upgrade().is_some() { + self.append_before_sibling(element, child); + } else { + self.append(previous, child); + } + } + fn append_doctype_to_document(&self, _: StrTendril, _: StrTendril, _: StrTendril) {} + fn get_template_contents(&self, target: &Handle) -> Handle { + match &target.kind { + Kind::Element { template: Some(contents), .. } => Rc::clone(contents), + _ => unreachable!("HTML tree builder requested a non-template's contents"), + } + } + fn is_mathml_annotation_xml_integration_point(&self, target: &Handle) -> bool { + matches!(&target.kind, Kind::Element { mathml_point: true, .. }) + } + fn add_attrs_if_missing(&self, target: &Handle, attrs: Vec) { + if let Kind::Element { attrs: existing, .. } = &target.kind { + let mut existing = existing.borrow_mut(); + for attr in attrs { + if !existing.iter().any(|a| a.name == attr.name) { + existing.push(attr); + } + } + } + } + fn remove_from_parent(&self, target: &Handle) { + DomNode::detach(target); + } + fn reparent_children(&self, node: &Handle, parent: &Handle) { + if Rc::ptr_eq(node, parent) { + return; + } + loop { + let Some(child) = node.links.borrow().first.upgrade() else { break }; + self.append(parent, NodeOrText::AppendNode(child)); + if self.failure.get().is_some() { + break; + } + } + } +} + +fn adapt(node: &Handle) -> Option { + match &node.kind { + Kind::Element { name, attrs, .. } => Some(Node::Elem(Element { + ns: (!name.ns.is_empty()).then(|| Rc::from(name.ns.as_ref())), + local: name.local.to_string(), + attrs: attrs + .borrow() + .iter() + .map(|attr| Attr { + ns: (!attr.name.ns.is_empty()).then(|| Rc::from(attr.name.ns.as_ref())), + local: attr.name.local.to_string(), + value: attr.value.to_string(), + }) + .collect(), + children: node.children().filter_map(|child| adapt(&child)).collect(), + })), + Kind::Text(text) => Some(Node::Text(text.borrow().to_string())), + Kind::Document | Kind::Ignored => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{AnchorId, Block, ImageSource, LinkTarget, inlines_to_plain_text}; + use crate::shared::html::{HtmlCtx, Stylesheet, to_blocks}; + + struct Ctx; + + impl HtmlCtx for Ctx { + fn link_target(&self, href: &str) -> Option { + Some(LinkTarget::Relative(href.into())) + } + fn image_source(&self, _: &str) -> Result, ConvertError> { + Ok(None) + } + fn anchor_id(&self, raw: &str) -> AnchorId { + raw.into() + } + } + + fn test_sink() -> Sink { + Sink::new() + } + + fn test_element(sink: &Sink, local: &str) -> Handle { + sink.create_element( + QualName::new(None, Default::default(), local.into()), + Vec::new(), + ElementFlags::default(), + ) + } + + fn assert_children(parent: &Handle, expected: &[&Handle]) { + let children: Vec<_> = parent.children().collect(); + assert_eq!(children.len(), expected.len()); + for (index, (child, expected)) in children.iter().zip(expected).enumerate() { + assert!(Rc::ptr_eq(child, expected)); + let links = child.links.borrow(); + assert!(links.parent.upgrade().is_some_and(|p| Rc::ptr_eq(&p, parent))); + let previous = links.previous.upgrade(); + assert_eq!(previous.is_some(), index > 0); + if let Some(previous) = previous { + assert!(Rc::ptr_eq(&previous, &children[index - 1])); + } + let next = links.next.upgrade(); + assert_eq!(next.is_some(), index + 1 < children.len()); + if let Some(next) = next { + assert!(Rc::ptr_eq(&next, &children[index + 1])); + } + } + let links = parent.links.borrow(); + for (actual, expected) in + [(links.first.upgrade(), children.first()), (links.last.upgrade(), children.last())] + { + assert_eq!(actual.is_some(), expected.is_some()); + if let (Some(actual), Some(expected)) = (actual, expected) { + assert!(Rc::ptr_eq(&actual, expected)); + } + } + } + + #[test] + fn sibling_moves_and_removals_preserve_both_directions() { + let sink = test_sink(); + let parent = sink.get_document(); + let nodes: Vec<_> = + ["a", "b", "c", "d"].iter().map(|local| test_element(&sink, local)).collect(); + for child in &nodes { + sink.append(&parent, NodeOrText::AppendNode(Rc::clone(child))); + } + sink.append_before_sibling(&nodes[1], NodeOrText::AppendNode(Rc::clone(&nodes[3]))); + assert_children(&parent, &[&nodes[0], &nodes[3], &nodes[1], &nodes[2]]); + sink.append_before_sibling(&nodes[2], NodeOrText::AppendNode(Rc::clone(&nodes[0]))); + assert_children(&parent, &[&nodes[3], &nodes[1], &nodes[0], &nodes[2]]); + sink.append_before_sibling(&nodes[0], NodeOrText::AppendNode(Rc::clone(&nodes[0]))); + sink.append(&parent, NodeOrText::AppendNode(Rc::clone(&nodes[3]))); + assert_children(&parent, &[&nodes[1], &nodes[0], &nodes[2], &nodes[3]]); + for (remove, expected) in [ + (1, vec![&nodes[0], &nodes[2], &nodes[3]]), + (2, vec![&nodes[0], &nodes[3]]), + (3, vec![&nodes[0]]), + (0, vec![]), + ] { + sink.remove_from_parent(&nodes[remove]); + sink.remove_from_parent(&nodes[remove]); + assert_children(&parent, &expected); + } + } + + #[test] + fn inserted_text_merges_only_with_its_previous_text_sibling() { + let sink = test_sink(); + let parent = sink.get_document(); + let table = test_element(&sink, "table"); + sink.append(&parent, NodeOrText::AppendNode(Rc::clone(&table))); + for text in ["one", "two"] { + sink.append_before_sibling(&table, NodeOrText::AppendText(text.into())); + } + for text in ["after", "+more"] { + sink.append(&parent, NodeOrText::AppendText(text.into())); + } + let children: Vec<_> = parent.children().collect(); + assert_children(&parent, &[&children[0], &table, &children[2]]); + let text = |node: &Handle| match &node.kind { + Kind::Text(text) => text.borrow().to_string(), + _ => panic!("expected text"), + }; + assert_eq!(text(&children[0]), "onetwo"); + assert_eq!(text(&children[2]), "after+more"); + let divider = test_element(&sink, "div"); + sink.append_before_sibling(&table, NodeOrText::AppendNode(Rc::clone(÷r))); + sink.append_before_sibling(&table, NodeOrText::AppendText("three".into())); + let children: Vec<_> = parent.children().collect(); + assert_children(&parent, &[&children[0], ÷r, &children[2], &table, &children[4]]); + assert_eq!(text(&children[2]), "three"); + } + + #[test] + fn reparenting_keeps_order_and_template_contents_alive_until_finish() { + let sink = test_sink(); + let source = test_element(&sink, "div"); + let target = test_element(&sink, "section"); + let nodes: Vec<_> = + ["a", "b", "c"].iter().map(|local| test_element(&sink, local)).collect(); + sink.append(&target, NodeOrText::AppendNode(Rc::clone(&nodes[0]))); + for child in &nodes[1..] { + sink.append(&source, NodeOrText::AppendNode(Rc::clone(child))); + } + sink.reparent_children(&source, &target); + sink.reparent_children(&target, &target); + assert_children(&source, &[]); + assert_children(&target, &[&nodes[0], &nodes[1], &nodes[2]]); + + let mut flags = ElementFlags::default(); + flags.template = true; + let template = sink.create_element( + QualName::new(None, Default::default(), "template".into()), + Vec::new(), + flags, + ); + let contents = sink.get_template_contents(&template); + sink.append(&contents, NodeOrText::AppendText("inside".into())); + let text = Rc::downgrade(&contents.children().next().unwrap()); + let document = sink.finish(); + assert!(text.upgrade().is_some()); + drop(document); + assert!(text.upgrade().is_none()); + } + + #[test] + fn malformed_tables_and_formatting_keep_html_repair_order() { + let tree = parse( + b"one
two
three
cell

fourfivesixseven", + encoding_rs::UTF_8, + ) + .unwrap(); + let body = tree.descendants_any("body").next().unwrap(); + assert_eq!(body.text(), "onetwothreecellfourfivesixseven"); + let paragraph = body.descendants_any("p").next().unwrap(); + let bold = paragraph + .children + .iter() + .find_map(|node| match node { + Node::Elem(element) if element.local == "b" => Some(element), + _ => None, + }) + .unwrap(); + assert_eq!(bold.text(), "fourfive"); + let italic: Vec<_> = paragraph.descendants_any("i").map(Element::text).collect(); + assert_eq!(italic, ["five", "six"]); + } + + #[test] + fn ignored_comments_do_not_break_sibling_links_or_text_merging() { + let tree = parse( + b"

beforeaftermiddlelast", + encoding_rs::UTF_8, + ) + .unwrap(); + let paragraph = tree.descendants_any("p").next().unwrap(); + assert_eq!(paragraph.text(), "beforeaftermiddlelast"); + assert_eq!(paragraph.children.len(), 3); + assert!(matches!(¶graph.children[0], Node::Text(text) if text == "beforeafter")); + } + + #[test] + fn dropping_a_wide_dom_releases_its_nodes_without_a_recursive_sibling_chain() { + let sink = test_sink(); + let parent = sink.get_document(); + let mut last = Weak::new(); + for _ in 0..32768 { + let child = test_element(&sink, "div"); + last = Rc::downgrade(&child); + sink.append(&parent, NodeOrText::AppendNode(child)); + } + let document = sink.finish(); + assert!(last.upgrade().is_some()); + drop(document); + assert!(last.upgrade().is_none()); + } + + #[test] + fn inserting_and_detaching_wide_siblings_does_not_walk_the_parent() { + let sink = test_sink(); + let parent = sink.get_document(); + let children: Vec<_> = (0..4096).map(|_| sink.node(Kind::Document)).collect(); + for child in &children { + sink.append(&parent, NodeOrText::AppendNode(Rc::clone(child))); + } + SIBLING_STEPS.with(|steps| steps.set(0)); + for _ in 0..1024 { + let child = sink.node(Kind::Document); + sink.append_before_sibling(&children[2048], NodeOrText::AppendNode(Rc::clone(&child))); + sink.remove_from_parent(&child); + } + // Foster parenting and adoption repair must not repeatedly scan or + // shift all the siblings preceding/following their insertion point. + let steps = SIBLING_STEPS.with(Cell::get); + assert!(steps <= 1024 * 4, "wide sibling mutation took {steps} sibling steps"); + assert!(sink.failure.get().is_none()); + } + + #[test] + fn old_html_recovers_void_tags_unquoted_attributes_and_omitted_end_tags() { + let tree = parse( + b"

One
two

Next ©

  • A
  • B
", + encoding_rs::WINDOWS_1252, + ) + .unwrap(); + let body = tree.descendants_any("body").next().unwrap(); + let blocks = to_blocks(body, &Stylesheet::default(), &Ctx).unwrap(); + let Block::Paragraph(first) = &blocks[0] else { panic!("first paragraph") }; + assert_eq!(inlines_to_plain_text(first), "One\ntwo"); + let Block::Paragraph(second) = &blocks[1] else { panic!("second paragraph") }; + assert_eq!(inlines_to_plain_text(second), "Next ©"); + let Block::List(list) = &blocks[2] else { panic!("list") }; + assert_eq!(list.items.len(), 2); + assert_eq!(body.descendants_any("p").next().unwrap().attr_any("id"), Some("first")); + } + + #[test] + fn html_meta_decodes_gbk_and_big5_without_an_xml_prolog() { + for (encoding, label, text) in [ + (encoding_rs::GBK, "gb2312", "中文说明:配置路由器"), + (encoding_rs::BIG5, "big5", "繁體中文說明"), + ] { + let input = format!( + "

{text}" + ); + let (bytes, _, errors) = encoding.encode(&input); + assert!(!errors); + let tree = parse(&bytes, encoding_rs::WINDOWS_1252).unwrap(); + assert!(tree.text().contains(text), "{}", tree.text()); + } + } + + #[test] + fn html_uses_the_book_encoding_when_no_charset_is_declared() { + let (bytes, _, _) = encoding_rs::GBK.encode("

配置路由器"); + let tree = parse(&bytes, encoding_rs::GBK).unwrap(); + assert!(tree.text().contains("配置路由器")); + } + + #[test] + fn ascii_html_retains_the_book_encoding_for_percent_encoded_links() { + for fallback in [encoding_rs::GBK, encoding_rs::BIG5, encoding_rs::WINDOWS_1250] { + let (_, selected) = + parse_with_encoding(b"Topic", fallback).unwrap(); + assert_eq!(selected, fallback); + } + } + + #[test] + fn undeclared_html_does_not_guess_utf8_from_ambiguous_legacy_bytes() { + for fallback in [encoding_rs::GBK, encoding_rs::BIG5, encoding_rs::WINDOWS_1250] { + let input = b"

\xc2\xa9"; + let (tree, selected) = parse_with_encoding(input, fallback).unwrap(); + let (expected, _, errors) = fallback.decode(b"\xc2\xa9"); + assert!(!errors); + assert_eq!(selected, fallback); + assert_eq!(tree.text(), expected); + } + } + + #[test] + fn explicit_html_encodings_take_precedence_over_the_book_encoding() { + for input in [ + "\u{feff}

é", + "

é", + "

é", + ] { + let (tree, selected) = parse_with_encoding(input.as_bytes(), encoding_rs::GBK).unwrap(); + assert_eq!(selected, encoding_rs::UTF_8); + assert!(tree.text().contains('é')); + } + } + + #[test] + fn stylesheets_use_css_charset_declarations_before_the_fallback() { + for (encoding, label, selector) in [ + (encoding_rs::GBK, "gbk", "隐藏"), + (encoding_rs::BIG5, "big5", "隱藏"), + (encoding_rs::WINDOWS_1250, "windows-1250", "ukryć"), + ] { + let input = format!("@charset \"{label}\"; .{selector} {{ display: none; }}"); + let (bytes, _, errors) = encoding.encode(&input); + assert!(!errors); + assert_eq!( + decode_stylesheet(&bytes, encoding_rs::WINDOWS_1252), + input.split_once(';').unwrap().1 + ); + } + } + + #[test] + fn stylesheet_bom_wins_over_charset_and_html_meta_is_not_a_declaration() { + assert_eq!( + decode_stylesheet( + "\u{feff}@charset \"big5\"; .隱藏 { display: none; }".as_bytes(), + encoding_rs::WINDOWS_1252, + ), + " .隱藏 { display: none; }" + ); + for prefix in [ + "/* */", + "/* @charset \"windows-1252\"; */", + "p::before { content: ''; }", + ] { + let input = format!("{prefix} .隐藏 {{ display: none; }}"); + let (bytes, _, errors) = encoding_rs::GBK.encode(&input); + assert!(!errors); + assert_eq!(decode_stylesheet(&bytes, encoding_rs::GBK), input); + } + } + + #[test] + fn css_charset_does_not_hide_the_first_rule_from_the_shared_stylesheet() { + for (encoding, prefix, selector) in [ + (encoding_rs::GBK, "@charset \"gbk\";", "隐藏"), + (encoding_rs::BIG5, "@charset \"big5\";", "隱藏"), + (encoding_rs::WINDOWS_1250, "@charset \"windows-1250\";", "ukryć"), + (encoding_rs::UTF_8, "\u{feff}@charset \"big5\";", "隱藏"), + ] { + let input = format!("{prefix} .{selector} {{ display: none; }}"); + let (bytes, _, errors) = encoding.encode(&input); + assert!(!errors); + let mut stylesheet = Stylesheet::default(); + stylesheet.add(&decode_stylesheet(&bytes, encoding_rs::WINDOWS_1252)); + let input = format!("

Hidden

Visible

"); + let tree = parse(input.as_bytes(), encoding_rs::UTF_8).unwrap(); + let body = tree.descendants_any("body").next().unwrap(); + let blocks = to_blocks(body, &stylesheet, &Ctx).unwrap(); + assert_eq!(blocks.len(), 1, "{prefix}"); + let Block::Paragraph(text) = &blocks[0] else { panic!("visible paragraph") }; + assert_eq!(inlines_to_plain_text(text), "Visible"); + } + } + + #[test] + fn malformed_or_misplaced_css_charset_text_is_preserved() { + for input in [ + "@charset \"unknown-codec\"; .hidden { display: none; }", + "@charset 'gbk'; .hidden { display: none; }", + "@charset \"gbk\" .hidden { display: none; }", + " @charset \"gbk\"; .hidden { display: none; }", + ] { + assert_eq!(decode_stylesheet(input.as_bytes(), encoding_rs::GBK), input); + } + } + + #[test] + fn charset_sniff_ignores_comments_and_script_text() { + for prefix in [ + "", + r#""#, + ] { + let input = format!("{prefix}

中文说明:配置路由器"); + let (bytes, _, errors) = encoding_rs::GBK.encode(&input); + assert!(!errors); + let tree = parse(&bytes, encoding_rs::WINDOWS_1252).unwrap(); + let body = tree.descendants_any("body").next().unwrap(); + assert!(body.text().contains("中文说明:配置路由器"), "{}", body.text()); + } + } + + #[test] + fn deeply_nested_html_is_a_resource_error() { + let input = format!("{}text{}", "

".repeat(300), "
".repeat(300)); + assert!(matches!( + parse(input.as_bytes(), encoding_rs::UTF_8), + Err(ConvertError::ResourceLimit { .. }) + )); + } +} diff --git a/src/formats/chm/lzx/LICENSE-MIT b/src/formats/chm/lzx/LICENSE-MIT new file mode 100644 index 00000000..7d19ec81 --- /dev/null +++ b/src/formats/chm/lzx/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Lonami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/formats/chm/lzx/README.md b/src/formats/chm/lzx/README.md new file mode 100644 index 00000000..0e0dc1bf --- /dev/null +++ b/src/formats/chm/lzx/README.md @@ -0,0 +1,10 @@ +This private decoder is adapted from `lzxd` 0.2.7 by Lonami, under the MIT +license reproduced in LICENSE-MIT. Upstream: https://codeberg.org/Lonami/lzxd + +CHM archives with only SpanInfo have no frame-offset table. The local adaptation +returns consumed input bytes for each decoded 32 KiB frame, caps raw reads at the +frame boundary, and consumes frame alignment/padding. Decoding remains sequential; +the Huffman tables, dictionary, and unfinished blocks survive between frames. +The E8 zero-target case is corrected, and dictionary resets preserve the absolute +E8 output position. Invalid match offsets return errors before copying window +bytes. The original decoder tests are retained. diff --git a/src/formats/chm/lzx/bitstream.rs b/src/formats/chm/lzx/bitstream.rs new file mode 100644 index 00000000..9c0eff71 --- /dev/null +++ b/src/formats/chm/lzx/bitstream.rs @@ -0,0 +1,327 @@ +/// > An LZXD bitstream is encoded as a sequence of aligned 16-bit integers stored in the +/// > least-significant- byte to most-significant-byte order, also known as byte-swapped, +/// > or little-endian, words. Given an input stream of bits named a, b, c,..., x, y, z, +/// > A, B, C, D, E, F, the output byte stream MUST be as [ 0| 1| 2| 3|...|30|31]. +/// +/// It is worth mentioning that older revisions of the document explain this better: +/// +/// > Given an input stream of bits named a, b, c, ..., x, y, z, A, B, C, D, E, F, the output +/// > byte stream (with byte boundaries highlighted) would be as follows: +/// > [i|j|k|l|m|n|o#p|a|b|c|d|e|f|g|h#y|z|A|B|C|D|E|F#q|r|s|t|u|v|w|x] +use super::DecodeFailed; + +pub struct Bitstream<'a> { + buffer: &'a [u8], + // Next number in the bitstream. + n: u16, + // How many bits left in the current `n`. + remaining: u8, +} + +impl<'a> Bitstream<'a> { + pub fn new(buffer: &'a [u8]) -> Self { + Self { buffer, n: 0, remaining: 0 } + } + + // Advance the buffer to the next 16-bit integer. + fn advance_buffer(&mut self) -> Result<(), DecodeFailed> { + if self.buffer.len() < 2 { + return Err(DecodeFailed::UnexpectedEof); + } + + self.remaining = 16; + self.n = u16::from_le_bytes([self.buffer[0], self.buffer[1]]); + self.buffer = &self.buffer[2..]; + Ok(()) + } + + pub fn read_bit(&mut self) -> Result { + if self.remaining == 0 { + self.advance_buffer()?; + } + + self.remaining -= 1; + self.n = self.n.rotate_left(1); + Ok(self.n & 1) + } + + pub fn read_byte(&mut self) -> Option { + if self.buffer.is_empty() { + return None; + } + let byte = self.buffer[0]; + self.buffer = &self.buffer[1..]; + Some(byte) + } + + /// Read from the bistream, no more than 16 bits (one word). + fn read_bits_oneword(&mut self, bits: u8) -> Result { + assert!(bits <= 16); + debug_assert!(self.remaining <= 16); + + Ok(if bits <= self.remaining { + self.remaining -= bits; + self.n = self.n.rotate_left(bits as u32); + self.n & ((1 << bits) - 1) + } else { + // No need to store `rol` result in `n` as we're about to overwrite it. + let hi = self.n.rotate_left(self.remaining as u32) & ((1 << self.remaining) - 1); + let bits = bits - self.remaining; + self.advance_buffer()?; + + self.remaining -= bits; + self.n = self.n.rotate_left(bits as u32); + // `bits` may be 16 which would overflow the left shift, operate on `u32` and trunc. + let lo = self.n & ((1u32 << bits) as u16).wrapping_sub(1); + + ((hi as u32) << bits) as u16 | lo + }) + } + + pub fn read_bits(&mut self, bits: u8) -> Result { + if bits <= 16 { + self.read_bits_oneword(bits).map(|w| w as u32) + } else { + assert!(bits <= 32); + + // Read the two words. + let w0 = self.read_bits_oneword(16)? as u32; + let w1 = self.read_bits_oneword(bits - 16)? as u32; + + Ok((w0 << (bits - 16)) | w1) + } + } + + /// Peek from the bitstream, no more than 16 bits (one word). + fn peek_bits_oneword(&self, bits: u8) -> u16 { + // Copy paste of `read_bits`, but without advancing the buffer. + assert!(bits <= 16); + + if bits <= self.remaining { + self.n.rotate_left(bits as u32) & ((1 << bits) - 1) + } else { + let hi = self.n.rotate_left(self.remaining as u32) & ((1 << self.remaining) - 1); + let bits = bits - self.remaining; + + // We may peek more than we need (i.e. at the end of a chunk), due to the way + // our decoder is implemented. This is a bit ugly but luckily we can pretend + // there are just zeros after. + let n = if self.buffer.len() < 2 { + 0 + } else { + u16::from_le_bytes([self.buffer[0], self.buffer[1]]) + }; + let lo = n.rotate_left(bits as u32) & ((1u32 << bits) as u16).wrapping_sub(1); + + ((hi as u32) << bits) as u16 | lo + } + } + + pub fn peek_bits(&self, bits: u8) -> u32 { + if bits <= 16 { + self.peek_bits_oneword(bits) as u32 + } else { + assert!(bits <= 32); + + // Read the two words. + let mut advanced_stream = + Self { buffer: self.buffer, n: self.n, remaining: self.remaining }; + let w0 = advanced_stream.read_bits_oneword(16).unwrap() as u32; + let w1 = advanced_stream.peek_bits_oneword(bits - 16) as u32; + + (w0 << (bits - 16)) | w1 + } + } + + pub fn read_u32_le(&mut self) -> Result { + let lo = self.read_bits_oneword(16)?.to_le_bytes(); + let hi = self.read_bits_oneword(16)?.to_le_bytes(); + Ok(u32::from_le_bytes([lo[0], lo[1], hi[0], hi[1]])) + } + + pub fn read_u24_be(&mut self) -> Result { + let hi = self.read_bits(16)?; + let lo = self.read_bits(8)?; + Ok(hi << 8 | lo) + } + + pub fn align(&mut self) -> Result<(), DecodeFailed> { + if self.remaining == 0 { + self.read_bits(16)?; + } else { + self.remaining = 0; + } + Ok(()) + } + + /// Copies from the current buffer to the destination output ignoring the representation. + pub fn read_raw(&mut self, output: &mut [u8]) -> Result<(), DecodeFailed> { + if self.buffer.len() < output.len() { + return Err(DecodeFailed::UnexpectedEof); + } + output.copy_from_slice(&self.buffer[..output.len()]); + self.buffer = &self.buffer[output.len()..]; + Ok(()) + } + + pub fn remaining_bytes(&self) -> usize { + self.buffer.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_sequential() { + // 0..=10 and padding using the least amount of bits possible, read LTR + let ns = [0b0110_1110_0101_1101_u16, 0b1110_0010_0110_1000_u16]; + let bit_lengths = [1u8, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4]; + + // Convert input sequence of 16-bit integers to byte-stream + let mut bytes = Vec::with_capacity(ns.len() * 2); + ns.iter().for_each(|n| bytes.extend(&n.to_le_bytes())); + + let mut bitstream = Bitstream::new(&bytes); + bit_lengths.iter().copied().enumerate().for_each(|(value, bit_length)| { + assert_eq!(bitstream.read_bits(bit_length), Ok(value as u32)); + }); + } + + #[test] + fn read_32le() { + let bytes = [0x56, 0x78, 0x12, 0x34]; + let mut bitstream = Bitstream::new(&bytes); + + assert_eq!(bitstream.read_u32_le(), Ok(873625686)); + } + + #[test] + fn read_24be() { + let ns = [0b0000_1100_0001_1000_u16, 0b0001_1000_0011_0000_u16]; + let mut bytes = Vec::with_capacity(ns.len() * 2); + ns.iter().for_each(|n| bytes.extend(&n.to_le_bytes())); + + let mut bitstream = Bitstream::new(&bytes); + + assert_eq!(bitstream.read_bits(4), Ok(0)); + assert_eq!(bitstream.read_u24_be(), Ok(0b1100_0001_1000_0001_1000_0011)); + assert_eq!(bitstream.read_bits(4), Ok(0)); + } + + #[test] + fn align() { + let bytes = [0b0100_0000, 0b0010_0000, 0b1000_0000, 0b0110_0000]; + let mut bitstream = Bitstream::new(&bytes); + + assert_eq!(bitstream.read_bits(3), Ok(1)); + bitstream.align().unwrap(); + assert_eq!(bitstream.read_bits(3), Ok(3)); + } + + #[test] + fn no_remain_after_aligned() { + let bytes = [0b0100_0000, 0b0010_0000, 0b1000_0000, 0b0110_0000]; + let mut bitstream = Bitstream::new(&bytes); + + bitstream.read_bits(3).unwrap(); + assert_ne!(bitstream.remaining, 0); + + bitstream.align().unwrap(); + assert_eq!(bitstream.remaining, 0); + + bitstream.read_bits(16).unwrap(); + assert_eq!(bitstream.remaining, 0); + } + + #[test] + fn check_read_bit() { + let bytes = [0b0110_1001, 0b1001_0110]; + let mut bitstream_1 = Bitstream::new(&bytes); + let mut bitstream_n = Bitstream::new(&bytes); + + (0..16).for_each(|_| { + assert_eq!(bitstream_1.read_bit().map(|b| b as u32), bitstream_n.read_bits(1)) + }); + } + + #[test] + fn read_bit_positions_match_description() { + // bits _abcdefgh_ijklmnop_qrstuvwx_yzABCDEF become: + let bit_indices: [u32; 32] = [ + 8, // i + 9, // j + 10, // k + 11, // l + 12, // m + 13, // n + 14, // o + 15, // p + 0, // a + 1, // b + 2, // c + 3, // d + 4, // e + 5, // f + 6, // g + 7, // h + 24, // y + 25, // z + 26, // A + 27, // B + 28, // C + 29, // D + 30, // E + 31, // F + 16, // q + 17, // r + 18, // s + 19, // t + 20, // u + 21, // v + 22, // w + 23, // x + ]; + for (index, bit_index) in bit_indices.iter().copied().enumerate() { + let n = 1u32.rotate_right(1).rotate_right(bit_index); + let bytes = n.to_be_bytes(); + eprintln!("index={index}, bit_index={bit_index}, bytes={n:032b}"); + + let mut bitstream = Bitstream::new(&bytes); + + if index != 0 { + assert_eq!(bitstream.read_bits(index as u8), Ok(0)); + } + + assert_eq!(bitstream.read_bit(), Ok(1)); + + if let Some(remaining) = 31usize.checked_sub(index) { + assert_eq!(bitstream.read_bits(remaining as u8), Ok(0)); + } + } + } + + #[test] + fn read_equals_peek() { + for index in 0..20 { + let n = (0b11_0111_0111_1101_1111_1101_1111_1111_1101_1111_1111_1111_u64) + .rotate_left(index); + + let bytes = n.to_be_bytes(); + for offset in 0..20 { + for size in 0..20 { + let mut bitstream = Bitstream::new(&bytes); + bitstream.read_bits(offset).unwrap(); + + let peeked = bitstream.peek_bits(size); + assert_eq!( + bitstream.read_bits(size), + Ok(peeked), + "offset={offset}, size={size}, bytes={n:032b}", + ); + } + } + } + } +} diff --git a/src/formats/chm/lzx/block.rs b/src/formats/chm/lzx/block.rs new file mode 100644 index 00000000..0820f6b6 --- /dev/null +++ b/src/formats/chm/lzx/block.rs @@ -0,0 +1,327 @@ +use super::{Bitstream, DecodeFailed, DecoderState, Tree}; + +// if position_slot < 4 { +// 0 +// } else if position_slot >= 36 { +// 17 +// } else { +// (position_slot - 2) / 2 +// } +const FOOTER_BITS: [u8; 289] = [ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, + 13, 14, 14, 15, 15, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, +]; + +// if position_slot == 0 { +// 0 +// } else { +// BASE_POSITION[position_slot - 1] + (1 << FOOTER_BITS[position_slot - 1]) +// } +const BASE_POSITION: [u32; 290] = [ + 0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, + 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 32768, 49152, 65536, 98304, 131072, 196608, + 262144, 393216, 524288, 655360, 786432, 917504, 1048576, 1179648, 1310720, 1441792, 1572864, + 1703936, 1835008, 1966080, 2097152, 2228224, 2359296, 2490368, 2621440, 2752512, 2883584, + 3014656, 3145728, 3276800, 3407872, 3538944, 3670016, 3801088, 3932160, 4063232, 4194304, + 4325376, 4456448, 4587520, 4718592, 4849664, 4980736, 5111808, 5242880, 5373952, 5505024, + 5636096, 5767168, 5898240, 6029312, 6160384, 6291456, 6422528, 6553600, 6684672, 6815744, + 6946816, 7077888, 7208960, 7340032, 7471104, 7602176, 7733248, 7864320, 7995392, 8126464, + 8257536, 8388608, 8519680, 8650752, 8781824, 8912896, 9043968, 9175040, 9306112, 9437184, + 9568256, 9699328, 9830400, 9961472, 10092544, 10223616, 10354688, 10485760, 10616832, 10747904, + 10878976, 11010048, 11141120, 11272192, 11403264, 11534336, 11665408, 11796480, 11927552, + 12058624, 12189696, 12320768, 12451840, 12582912, 12713984, 12845056, 12976128, 13107200, + 13238272, 13369344, 13500416, 13631488, 13762560, 13893632, 14024704, 14155776, 14286848, + 14417920, 14548992, 14680064, 14811136, 14942208, 15073280, 15204352, 15335424, 15466496, + 15597568, 15728640, 15859712, 15990784, 16121856, 16252928, 16384000, 16515072, 16646144, + 16777216, 16908288, 17039360, 17170432, 17301504, 17432576, 17563648, 17694720, 17825792, + 17956864, 18087936, 18219008, 18350080, 18481152, 18612224, 18743296, 18874368, 19005440, + 19136512, 19267584, 19398656, 19529728, 19660800, 19791872, 19922944, 20054016, 20185088, + 20316160, 20447232, 20578304, 20709376, 20840448, 20971520, 21102592, 21233664, 21364736, + 21495808, 21626880, 21757952, 21889024, 22020096, 22151168, 22282240, 22413312, 22544384, + 22675456, 22806528, 22937600, 23068672, 23199744, 23330816, 23461888, 23592960, 23724032, + 23855104, 23986176, 24117248, 24248320, 24379392, 24510464, 24641536, 24772608, 24903680, + 25034752, 25165824, 25296896, 25427968, 25559040, 25690112, 25821184, 25952256, 26083328, + 26214400, 26345472, 26476544, 26607616, 26738688, 26869760, 27000832, 27131904, 27262976, + 27394048, 27525120, 27656192, 27787264, 27918336, 28049408, 28180480, 28311552, 28442624, + 28573696, 28704768, 28835840, 28966912, 29097984, 29229056, 29360128, 29491200, 29622272, + 29753344, 29884416, 30015488, 30146560, 30277632, 30408704, 30539776, 30670848, 30801920, + 30932992, 31064064, 31195136, 31326208, 31457280, 31588352, 31719424, 31850496, 31981568, + 32112640, 32243712, 32374784, 32505856, 32636928, 32768000, 32899072, 33030144, 33161216, + 33292288, 33423360, +]; + +struct DecodeInfo<'a> { + aligned_offset_tree: Option<&'a Tree>, + main_tree: &'a Tree, + length_tree: Option<&'a Tree>, +} + +#[derive(Debug)] +pub enum Decoded { + Single(u8), + Match { offset: usize, length: usize }, + Read(usize), +} + +#[derive(Debug)] +pub enum Kind { + Verbatim { main_tree: Tree, length_tree: Option }, + AlignedOffset { aligned_offset_tree: Tree, main_tree: Tree, length_tree: Option }, + Uncompressed { r: [u32; 3] }, +} + +/// Note that this is not the block header, but the head of the block's body, which includes +/// everything except the tail of the block data (either uncompressed data or token sequence). +pub struct Block { + /// Only 24 bits may be used. + pub remaining: u32, + pub size: u32, + pub kind: Kind, +} + +/// Read the pretrees for the main and length tree, and with those also read the trees +/// themselves, using the path lengths from a previous tree if any. +/// +/// This is used when reading a verbatim or aligned block. +fn read_main_and_length_trees( + bitstream: &mut Bitstream, + state: &mut DecoderState, +) -> Result<(), DecodeFailed> { + // Verbatim block + // Entry Comments + // Pretree for first 256 elements of main tree 20 elements, 4 bits each + // Path lengths of first 256 elements of main tree Encoded using pretree + // Pretree for remainder of main tree 20 elements, 4 bits each + // Path lengths of remaining elements of main tree Encoded using pretree + // Pretree for length tree 20 elements, 4 bits each + // Path lengths of elements in length tree Encoded using pretree + // Token sequence (matches and literals) Specified in section 2.6 + + state.main_tree.update_range_with_pretree(bitstream, 0..256)?; + + state + .main_tree + .update_range_with_pretree(bitstream, 256..256 + 8 * state.window_size.position_slots())?; + + state.length_tree.update_range_with_pretree(bitstream, 0..249)?; + + Ok(()) +} + +fn decode_element( + bitstream: &mut Bitstream, + r: &mut [u32; 3], + DecodeInfo { aligned_offset_tree, main_tree, length_tree }: DecodeInfo, +) -> Result { + // Decoding Matches and Literals (Aligned and Verbatim Blocks) + let main_element = main_tree.decode_element(bitstream)?; + + // Check if it is a literal character. + Ok(if main_element < 256 { + // It is a literal, so copy the literal to output. + Decoded::Single(main_element as u8) + } else { + // Decode the match. For a match, there are two components, offset and length. + let length_header = (main_element - 256) & 7; + + let match_length = if length_header == 7 { + // Length of the footer. + length_tree.ok_or(DecodeFailed::EmptyTree)?.decode_element(bitstream)? + 7 + 2 + } else { + length_header + 2 // no length footer + // Decoding a match length (if a match length < 257). + }; + assert_ne!(match_length, 0); + + let position_slot = (main_element - 256) >> 3; + + // Check for repeated offsets (positions 0, 1, 2). + let match_offset; + if position_slot == 0 { + match_offset = r[0]; + } else if position_slot == 1 { + match_offset = r[1]; + r.swap(0, 1); + } else if position_slot == 2 { + match_offset = r[2]; + r.swap(0, 2); + } else { + // Not a repeated offset. + let offset_bits = FOOTER_BITS[position_slot as usize]; + + let formatted_offset = if let Some(aligned_offset_tree) = aligned_offset_tree.as_ref() { + let verbatim_bits; + let aligned_bits; + + // This means there are some aligned bits. + if offset_bits >= 3 { + verbatim_bits = bitstream.read_bits(offset_bits - 3)? << 3; + aligned_bits = aligned_offset_tree.decode_element(bitstream)?; + } else { + // 0, 1, or 2 verbatim bits + verbatim_bits = bitstream.read_bits(offset_bits)?; + aligned_bits = 0; + } + + BASE_POSITION[position_slot as usize] + verbatim_bits + aligned_bits as u32 + } else { + // Block_type is a verbatim_block. + let verbatim_bits = bitstream.read_bits(offset_bits)?; + BASE_POSITION[position_slot as usize] + verbatim_bits + }; + + // Decoding a match offset. + match_offset = formatted_offset - 2; + + // Update repeated offset least recently used queue. + r[2] = r[1]; + r[1] = r[0]; + r[0] = match_offset; + } + + // Check for extra length. + // > If the match length is 257 or larger, the encoded match length token + // > (or match length, as specified in section 2.6) value is 257, and an + // > encoded Extra Length field follows the other match encoding components, + // > as specified in section 2.6.7, in the bitstream. + + // TODO for some reason, if we do this, parsing .xnb files with window size + // 64KB, it breaks and stops decompressing correctly, but no idea why. + /* + let match_length = if match_length == 257 { + // Decode the extra length. + let extra_len = if bitstream.read_bit() != 0 { + if bitstream.read_bit() != 0 { + if bitstream.read_bit() != 0 { + // > Prefix 0b111; Number of bits to decode 15; + bitstream.read_bits(15) + } else { + // > Prefix 0b110; Number of bits to decode 12; + bitstream.read_bits(12) + 1024 + 256 + } + } else { + // > Prefix 0b10; Number of bits to decode 10; + bitstream.read_bits(10) + 256 + } + } else { + // > Prefix 0b0; Number of bits to decode 8; + bitstream.read_bits(8) + }; + + // Get the match length (if match length >= 257). + // In all cases, + // > Base value to add to decoded value 257 + … + 257 + extra_len + } else { + match_length as u16 + }; + */ + + // Get match length and offset. Perform copy and paste work. + Decoded::Match { offset: match_offset as usize, length: match_length as usize } + }) +} + +impl Block { + pub(crate) fn read( + bitstream: &mut Bitstream, + state: &mut DecoderState, + ) -> Result { + // > Each block of compressed data begins with a 3-bit Block Type field. + // > Of the eight possible values, only three are valid values for the Block Type + // > field. + let kind = bitstream.read_bits(3)? as u8; + let size = bitstream.read_u24_be()?; + if size == 0 { + return Err(DecodeFailed::InvalidBlockSize(size)); + } + + let kind = match kind { + 0b001 => { + read_main_and_length_trees(bitstream, state)?; + + Kind::Verbatim { + main_tree: state.main_tree.create_instance()?, + length_tree: state.length_tree.create_instance_allow_empty()?, + } + } + 0b010 => { + // > encoding only the delta path lengths between the current and previous trees + // + // This means we don't need to worry about deltas on this tree. + let aligned_offset_tree = { + let mut path_lengths = Vec::with_capacity(8); + for _ in 0..8 { + path_lengths.push(bitstream.read_bits(3)? as u8); + } + + Tree::from_path_lengths(path_lengths)? + }; + + // > An aligned offset block is identical to the verbatim block except for the + // > presence of the aligned offset tree preceding the other trees. + read_main_and_length_trees(bitstream, state)?; + + Kind::AlignedOffset { + aligned_offset_tree, + main_tree: state.main_tree.create_instance()?, + length_tree: state.length_tree.create_instance_allow_empty()?, + } + } + 0b011 => { + bitstream.align()?; + Kind::Uncompressed { + r: [ + bitstream.read_u32_le()?, + bitstream.read_u32_le()?, + bitstream.read_u32_le()?, + ], + } + } + _ => return Err(DecodeFailed::InvalidBlock(kind)), + }; + + Ok(Block { remaining: size, size, kind }) + } + + pub(crate) fn decode_element( + &self, + bitstream: &mut Bitstream, + r: &mut [u32; 3], + ) -> Result { + match &self.kind { + Kind::Verbatim { main_tree, length_tree } => decode_element( + bitstream, + r, + DecodeInfo { + aligned_offset_tree: None, + main_tree, + length_tree: length_tree.as_ref(), + }, + ), + Kind::AlignedOffset { aligned_offset_tree, main_tree, length_tree } => decode_element( + bitstream, + r, + DecodeInfo { + aligned_offset_tree: Some(aligned_offset_tree), + main_tree, + length_tree: length_tree.as_ref(), + }, + ), + Kind::Uncompressed { r: new_r } => { + r.copy_from_slice(new_r); + Ok(Decoded::Read(self.remaining as usize)) + } + } + } +} diff --git a/src/formats/chm/lzx/mod.rs b/src/formats/chm/lzx/mod.rs new file mode 100644 index 00000000..507f3e3c --- /dev/null +++ b/src/formats/chm/lzx/mod.rs @@ -0,0 +1,507 @@ +//! This library implements the LZX compression format as described in +//! [LZX DELTA Compression and Decompression], revision 9.0. +//! +//! Lempel-Ziv Extended (LZX) is an LZ77-based compression engine, as described in [UASDC], +//! that is a universal lossless data compression algorithm. It performs no analysis on the +//! data. +//! +//! Lempel-Ziv Extended Delta (LZXD) is a derivative of the Lempel-Ziv Extended (LZX) format with +//! some modifications to facilitate efficient delta compression. +//! +//! In order to use this module, refer to the main [`Lzxd`] type and its methods. +//! +//! [LZX DELTA Compression and Decompression]: https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-patch/cc78752a-b4af-4eee-88cb-01f4d8a4c2bf +//! [UASDC]: https://ieeexplore.ieee.org/document/1055714 +//! [`Lzxd`]: struct.Lzxd.html +use std::{fmt, mem}; + +pub(crate) use bitstream::Bitstream; +pub(crate) use block::{Block, Decoded, Kind as BlockKind}; +pub(crate) use tree::{CanonicalTree, Tree}; +use window::Window; +pub use window::WindowSize; + +mod bitstream; +mod block; +mod tree; +mod window; + +/// A chunk represents exactly 32 KB of uncompressed data until the last chunk in the stream, +/// which can represent less than 32 KB. +pub const MAX_CHUNK_SIZE: usize = 32 * 1024; + +/// Decoder state needed for new blocks. +// TODO not sure how much we want to keep in DecoderState and Lzxd respectively +pub(crate) struct DecoderState { + /// The window size we're working with. + window_size: WindowSize, + + /// This tree cannot be used directly, it exists only to apply the delta of upcoming trees + /// to its path lengths. + main_tree: CanonicalTree, + + /// This tree cannot be used directly, it exists only to apply the delta of upcoming trees + /// to its path lengths. + length_tree: CanonicalTree, +} + +struct PostProcessState { + /// The pointer in the file at which to stop performing E8 translation. + e8_translation_size: i32, + + /// A buffer that can be used to hold postprocessed chunks. + data_chunk: Box<[u8]>, +} + +/// The main interface to perform LZXD decompression. +/// +/// This structure stores the required state to process the compressed chunks of data in a +/// sequential order. +/// +pub struct Lzxd { + /// Sliding window into which data is decompressed. + window: Window, + + /// Current decoder state. + state: DecoderState, + + /// > The three most recent real match offsets are kept in a list. + r: [u32; 3], + + /// The current offset into the decompressed data. + chunk_offset: usize, + + /// Match history available since the most recent dictionary reset. + history_len: usize, + + /// Has the very first chunk been read yet? Unlike the rest, it has additional data. + first_chunk_read: bool, + + /// Current block. + current_block: Block, + + /// Information and data related to E8 postprocessing. This is populated after + /// the first chunk is read. + postprocess: Option, +} + +/// Specific cause for decompression failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeFailed { + /// The chunk data caused a read of more items than the current block had in a single step. + OverreadBlock, + + /// There was not enough data in the chunk to fully decode, and a premature end was found. + UnexpectedEof, + + /// An invalid block type was found. + InvalidBlock(u8), + + /// An invalid block size was found. + InvalidBlockSize(u32), + + /// An invalid pretree element was found. + InvalidPretreeElement(u16), + + /// Invalid pretree run-length encoding. + InvalidPretreeRle, + + /// When attempting to construct a decode tree, we encountered an invalid path length tree. + InvalidPathLengths, + + /// A required decode tree was empty (all path lengths were 0). + EmptyTree, + + /// A match references zero, unavailable history, or bytes outside the window. + InvalidMatchOffset, + + /// The given window size was too small. + WindowTooSmall, + + /// Tried to read a chunk longer than [`MAX_CHUNK_SIZE`]. + /// + /// [`MAX_CHUNK_SIZE`]: constant.MAX_CHUNK_SIZE.html + ChunkTooLong, +} + +impl fmt::Display for DecodeFailed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use DecodeFailed::*; + + match self { + OverreadBlock => { + write!(f, "read more items than available in the block in a single step") + } + UnexpectedEof => write!(f, "reached end of chunk without fully decoding it"), + InvalidBlock(kind) => write!(f, "block type {} is invalid", kind), + InvalidBlockSize(size) => write!(f, "block size {} is invalid", size), + InvalidPretreeElement(elem) => write!(f, "found invalid pretree element {}", elem), + InvalidPretreeRle => write!(f, "found invalid pretree rle element"), + InvalidPathLengths => write!(f, "encountered invalid path lengths"), + EmptyTree => write!(f, "encountered empty decode tree"), + InvalidMatchOffset => write!(f, "match offset is outside the available history"), + WindowTooSmall => write!(f, "decode window was too small"), + ChunkTooLong => write!(f, "tried reading a chunk longer than {} bytes", MAX_CHUNK_SIZE), + } + } +} + +impl std::error::Error for DecodeFailed {} + +/// The error type used when decompression fails. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct DecompressError(DecodeFailed); + +impl fmt::Display for DecompressError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for DecompressError {} + +impl From for DecompressError { + fn from(value: DecodeFailed) -> Self { + Self(value) + } +} + +impl Lzxd { + /// Creates a new instance of the LZXD decoder state. The [`WindowSize`] must be obtained + /// from elsewhere (e.g. it may be predetermined to a certain value), and if it's wrong, + /// the decompressed values won't be those expected. + /// + /// [`WindowSize`]: enum.WindowSize.html + pub fn new(window_size: WindowSize) -> Self { + // > The main tree comprises 256 elements that correspond to all possible 8-bit + // > characters, plus 8 * NUM_POSITION_SLOTS elements that correspond to matches. + let main_tree = CanonicalTree::new(256 + 8 * window_size.position_slots()); + + // > The length tree comprises 249 elements. + let length_tree = CanonicalTree::new(249); + + Self { + window: window_size.create_buffer(), + // > Because trees are output several times during compression of large amounts of + // > data (multiple blocks), LZXD optimizes compression by encoding only the delta + // > path lengths lengths between the current and previous trees. + // + // Because it uses deltas, we need to store the previous value across blocks. + state: DecoderState { window_size, main_tree, length_tree }, + // > The initial state of R0, R1, R2 is (1, 1, 1). + r: [1, 1, 1], + first_chunk_read: false, + chunk_offset: 0, + history_len: 0, + postprocess: None, + // Start with some dummy value. + current_block: Block { + remaining: 0, + size: 0, + kind: BlockKind::Uncompressed { r: [1, 1, 1] }, + }, + } + } + + /// Try reading the header for the first chunk. + fn try_read_first_chunk(&mut self, bitstream: &mut Bitstream) -> Result<(), DecodeFailed> { + // > The first bit in the first chunk in the LZXD bitstream (following the 2-byte, + // > chunk-size prefix described in section 2.2.1) indicates the presence or absence of + // > two 16-bit fields immediately following the single bit. If the bit is set, E8 + // > translation is enabled. + if !self.first_chunk_read { + self.first_chunk_read = true; + + let e8_translation = bitstream.read_bit()? != 0; + self.postprocess = if e8_translation { + Some(PostProcessState { + data_chunk: vec![0; MAX_CHUNK_SIZE].into_boxed_slice(), + e8_translation_size: bitstream.read_bits(32)? as i32, + }) + } else { + None + }; + } + + Ok(()) + } + + /// Attempts to perform post-decompression E8 fixups on an output data buffer. + fn postprocess( + translation_size: i32, + chunk_offset: usize, + idata: &mut [u8], + ) -> Result<&[u8], DecodeFailed> { + let mut processed = 0usize; + + // Find the next E8 match, or finish once there are no more E8 matches. + while let Some(pos) = + idata[processed..].iter().position(|&e| e == 0xE8).map(|pos| processed + pos) + { + // N.B: E8 fixups are only performed for up to 10 bytes before the end of a chunk. + if idata.len() - pos <= 10 { + break; + } + + // This is the current file output pointer. + let current_pointer = chunk_offset + pos; + + // Match. Fix up the following bytes. + let abs_val = i32::from_le_bytes([ + idata[pos + 1], + idata[pos + 2], + idata[pos + 3], + idata[pos + 4], + ]); + if (abs_val >= -(current_pointer as i32)) && abs_val < translation_size { + let rel_val = if abs_val >= 0 { + abs_val.wrapping_sub(current_pointer as i32) + } else { + abs_val.wrapping_add(translation_size) + }; + + idata[pos + 1..pos + 5].copy_from_slice(&rel_val.to_le_bytes()); + } + + processed = pos + 5; + } + + Ok(idata) + } + + /// Decompresses the next compressed `chunk` from the LZXD data stream. + pub fn decompress_next_stream( + &mut self, + chunk: &[u8], + output_len: usize, + ) -> Result<(&[u8], usize), DecompressError> { + if output_len > MAX_CHUNK_SIZE { + return Err(DecodeFailed::ChunkTooLong.into()); + } + // > A chunk represents exactly 32 KB of uncompressed data until the last chunk in the + // > stream, which can represent less than 32 KB. + // + // > The LZXD engine encodes a compressed, chunk-size prefix field preceding each + // > compressed chunk in the compressed byte stream. The compressed, chunk-size prefix + // > field is a byte aligned, little-endian, 16-bit field. + // + // However, this doesn't seem to be part of LZXD itself? At least when testing with + // `.xnb` files, every chunk comes with a compressed chunk size unless it has the flag + // set to 0xff where it also includes the uncompressed chunk size. + // + // TODO maybe the docs could clarify whether this length is compressed or not + + let mut bitstream = Bitstream::new(chunk); + + self.try_read_first_chunk(&mut bitstream)?; + + let mut decoded_len = 0; + while decoded_len != output_len { + if self.current_block.remaining == 0 { + // Re-align the bitstream to word + // Related: https://github.com/GNOME/gcab/blob/master/libgcab/decomp.c#L883. + // Related: https://github.com/kyz/libmspack/blob/master/libmspack/mspack/lzxd.c#L469 + if matches!(self.current_block.kind, BlockKind::Uncompressed { .. }) + && !self.current_block.size.is_multiple_of(2) + { + bitstream.read_byte(); + } + self.current_block = Block::read(&mut bitstream, &mut self.state)?; + assert_ne!(self.current_block.remaining, 0); + } + + let decoded = self.current_block.decode_element(&mut bitstream, &mut self.r)?; + + let advance = match decoded { + Decoded::Single(value) => { + self.window.push(value); + 1 + } + Decoded::Match { offset, length } => { + if offset == 0 + || offset > self.state.window_size as usize + || offset > self.history_len + decoded_len + { + return Err(DecodeFailed::InvalidMatchOffset.into()); + } + self.window.copy_from_self(offset, length); + length + } + Decoded::Read(length) => { + // Read up to end of chunk, to allow for larger blocks. + let length = + length.min(bitstream.remaining_bytes()).min(output_len - decoded_len); + // Will re-align if needed, just as decompressed reads mandate. + self.window.copy_from_bitstream(&mut bitstream, length)?; + length + } + }; + + if advance == 0 { + return Err(DecodeFailed::UnexpectedEof.into()); + } + + if advance > output_len - decoded_len { + return Err(DecodeFailed::OverreadBlock.into()); + } + decoded_len += advance; + if let Some(value) = self.current_block.remaining.checked_sub(advance as u32) { + self.current_block.remaining = value; + } else { + return Err(DecodeFailed::OverreadBlock.into()); + } + } + + // Each compressed frame ends on a 16-bit boundary; the bit reader has + // already fetched its last padded word. Odd uncompressed blocks carry + // one additional padding byte, including when they end at the boundary. + if self.current_block.remaining == 0 + && matches!(self.current_block.kind, BlockKind::Uncompressed { .. }) + && !self.current_block.size.is_multiple_of(2) + { + bitstream.read_byte().ok_or(DecodeFailed::UnexpectedEof)?; + self.current_block.size = 0; + } + let consumed = chunk.len() - bitstream.remaining_bytes(); + let chunk_offset = self.chunk_offset; + self.chunk_offset += decoded_len; + self.history_len = (self.history_len + decoded_len).min(self.state.window_size as usize); + + let view = self.window.past_view(decoded_len)?; + if let Some(postprocess) = self.postprocess.as_mut() { + // E8 fixups are disabled after 1GB of input data, + // or if the chunk size is too small. + if chunk_offset >= 0x4000_0000 || decoded_len <= 10 { + Ok((view, consumed)) + } else { + let postprocess_buf = &mut postprocess.data_chunk[..decoded_len]; + postprocess_buf.copy_from_slice(view); + + // E8 fixups are enabled. Postprocess the output buffer. + let view = Self::postprocess( + postprocess.e8_translation_size, + chunk_offset, + postprocess_buf, + )?; + Ok((view, consumed)) + } + } else { + Ok((view, consumed)) + } + } + + #[cfg(test)] + fn decompress_next( + &mut self, + chunk: &[u8], + output_len: usize, + ) -> Result<&[u8], DecompressError> { + self.decompress_next_stream(chunk, output_len).map(|(bytes, _)| bytes) + } + + /// Reset compression state while preserving the absolute E8 output position. + pub fn reset(&mut self) { + let mut this = Self::new(self.state.window_size); + this.chunk_offset = self.chunk_offset; + let _ = mem::replace(self, this); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_uncompressed() { + let data = [ + 0x00, 0x30, 0x30, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, b'a', b'b', b'c', 0x00, + ]; + + let mut lzxd = Lzxd::new(WindowSize::KB32); // size does not matter + let res = lzxd.decompress_next(&data, 3); + assert_eq!(res.unwrap(), [b'a', b'b', b'c']); + } + + #[test] + fn reset() { + let data = [ + 0x00, 0x30, 0x30, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, b'a', b'b', b'c', 0x00, + ]; + + let mut lzxd = Lzxd::new(WindowSize::KB32); // size does not matter + let res = lzxd.decompress_next(&data, 3); + assert_eq!(res.unwrap(), [b'a', b'b', b'c']); + + lzxd.reset(); + let res = lzxd.decompress_next(&data, 3); + assert_eq!(res.unwrap(), [b'a', b'b', b'c']); + } + + #[test] + fn check_e8() { + let data = [ + 0x5B, 0x80, 0x80, 0x8D, 0x00, 0x30, 0x80, 0x0A, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x54, 0x68, 0x69, 0x73, 0x20, 0x66, 0x69, 0x6C, + 0x65, 0x20, 0x68, 0x61, 0x73, 0x20, 0x61, 0x6E, 0x20, 0x45, 0x38, 0x20, 0x62, 0x79, + 0x74, 0x65, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x45, 0x38, 0x20, + 0x74, 0x72, 0x61, 0x6E, 0x73, 0x6C, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2C, 0x20, 0x58, + 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0xE8, 0x7B, + 0x00, 0x00, 0x00, 0xE8, 0x7B, 0x00, 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + ]; + + let mut lzxd = Lzxd::new(WindowSize::KB32); + let res = lzxd.decompress_next(&data, 168); + assert_eq!( + res.unwrap(), + b"This file has an E8 byte to test E8 translation, Xdddddddddddddddd\ + dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\ + dddddddddddddd\xE8\xE9\xFF\xFF\xFF\xE8\xE4\xFF\xFF\xFFdddddddddddd" + ); + } + + #[test] + fn uncompressed_single_byte_data() { + let mut lzxd = Lzxd::new(WindowSize::KB32); + let res = lzxd.decompress_next(&[0x00], 32); + assert_eq!(res.unwrap_err().0, DecodeFailed::UnexpectedEof); + } + + #[test] + fn uncompressed_advance_zero() { + // Regression test for "assertion `left != right` failed: left: 0, right: 0" panic + // This occurs when the decoder cannot advance (advance == 0) + // Data from fuzzer crash + let data = vec![ + 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, + ]; + + let mut lzxd = Lzxd::new(WindowSize::KB32); + let res = lzxd.decompress_next(&data, 32); + assert_eq!(res.unwrap_err().0, DecodeFailed::UnexpectedEof); + } + + #[test] + fn peek_bits_oneword_bounds_check() { + // Regression test for peek_bits_oneword index out of bounds panic + // This exercises the buffer.len() < 2 check in peek_bits_oneword + // Data from fuzzer crash + let data = vec![ + 0x12, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x12, 0x8a, + ]; + + let mut lzxd = Lzxd::new(WindowSize::KB32); + let res = lzxd.decompress_next(&data, 32); + assert_eq!(res.unwrap_err().0, DecodeFailed::UnexpectedEof); + } +} diff --git a/src/formats/chm/lzx/tree.rs b/src/formats/chm/lzx/tree.rs new file mode 100644 index 00000000..553bf061 --- /dev/null +++ b/src/formats/chm/lzx/tree.rs @@ -0,0 +1,269 @@ +use std::fmt; +use std::num::NonZeroU8; +use std::ops::Range; + +use super::{Bitstream, DecodeFailed}; + +/// The canonical tree cannot be used to decode elements. Instead, it behaves like a builder for +/// instances of the actual tree that can decode elements efficiently. +#[derive(Debug)] +pub struct CanonicalTree { + // > Each tree element can have a path length of [0, 16], where a zero path length indicates + // > that the element has a zero frequency and is not present in the tree. + // + // We represent them as `u8` due to their very short range. + path_lengths: Vec, +} + +pub struct Tree { + path_lengths: Vec, + largest_length: NonZeroU8, + huffman_tree: Vec, +} + +impl CanonicalTree { + pub fn new(count: usize) -> Self { + Self { + // > In the case of the very first such tree, the delta is calculated against a tree + // > in which all elements have a zero path length. + path_lengths: vec![0; count], + } + } + + /// Create a new `Tree` instance from this cast that can be used to decode elements. If the + /// resulting tree is empty (all path lengths are 0), then `Ok(None)` is returned. + /// + /// This method transforms the canonical Huffman tree into a different structure that can + /// be used to better decode elements. + // > an LZXD decoder uses only the path lengths of the Huffman tree to reconstruct the + // > identical tree, + pub fn create_instance_allow_empty(&self) -> Result, DecodeFailed> { + // The ideas implemented by this method are heavily inspired from LeonBlade's xnbcli + // on GitHub. + // + // The path lengths contains the bit indices or zero if its not present, so find the + // highest path length to determine how big our tree needs to be. + let largest_length = + match NonZeroU8::new(*self.path_lengths.iter().max().expect("empty path lengths")) { + Some(x) => x, + // N.B: If all the path lengths are zero, then the tree is empty (which is allowed). + None => return Ok(None), + }; + let mut huffman_tree = vec![0; 1 << largest_length.get()]; + + // > a zero path length indicates that the element has a zero frequency and is not + // > present in the tree. Tree elements are output in sequential order starting with the + // > first element + // + // We start at the MSB, 1, and write the tree elements in sequential order from index 0. + let mut pos = 0; + for bit in 1..=largest_length.get() { + let amount = 1 << (largest_length.get() - bit); + + // The codes correspond with the indices of the path length (because + // `path_lengths[code]` is its path length). + for code in 0..self.path_lengths.len() { + // As soon as a code's path length matches with our bit index write the code as + // many times as the bit index itself represents. + if self.path_lengths[code] == bit { + huffman_tree + .get_mut(pos..pos + amount) + .ok_or(DecodeFailed::InvalidPathLengths)? + .iter_mut() + .for_each(|x| *x = code as u16); + + pos += amount; + } + } + } + + // If we didn't fill the entire table, the path lengths were wrong. + if pos != huffman_tree.len() { + Err(DecodeFailed::InvalidPathLengths)?; + } + + Ok(Some(Tree { path_lengths: self.path_lengths.clone(), largest_length, huffman_tree })) + } + + /// Create a new `Tree` instance from this cast that can be used to decode elements. + /// + /// This method transforms the canonical Huffman tree into a different structure that can + /// be used to better decode elements. + // > an LZXD decoder uses only the path lengths of the Huffman tree to reconstruct the + // > identical tree, + pub fn create_instance(&self) -> Result { + self.create_instance_allow_empty()?.ok_or(DecodeFailed::EmptyTree) + } + + // Note: the tree already exists and is used to apply the deltas. + pub fn update_range_with_pretree( + &mut self, + bitstream: &mut Bitstream, + range: Range, + ) -> Result<(), DecodeFailed> { + // > Each of the 17 possible values of (len[x] - prev_len[x]) mod 17, plus three + // > additional codes used for run-length encoding, are not output directly as 5-bit + // > numbers but are instead encoded via a Huffman tree called the pretree. The pretree + // > is generated dynamically according to the frequencies of the 20 allowable tree + // > codes. The structure of the pretree is encoded in a total of 80 bits by using 4 bits + // > to output the path length of each of the 20 pretree elements. Once again, a zero + // > path length indicates a zero-frequency element. + let pretree = { + let mut path_lengths = Vec::with_capacity(20); + for _ in 0..20 { + path_lengths.push(bitstream.read_bits(4)? as u8) + } + + Tree::from_path_lengths(path_lengths)? + }; + + // > Tree elements are output in sequential order starting with the first element. + let mut i = range.start; + while i < range.end { + // > The "real" tree is then encoded using the pretree Huffman codes. + let code = pretree.decode_element(bitstream)?; + + // > Elements can be encoded in one of two ways: if several consecutive elements have + // > the same path length, run-length encoding is employed; otherwise, the element is + // > output by encoding the difference between the current path length and the + // > previous path length of the tree, mod 17. + match code { + 0..=16 => { + self.path_lengths[i] = (17 + self.path_lengths[i] - code as u8) % 17; + i += 1; + } + // > Codes 17, 18, and 19 are used to represent consecutive elements that have the + // > same path length. + 17 => { + let zeros = bitstream.read_bits(4)?; + self.path_lengths + .get_mut(i..i + zeros as usize + 4) + .ok_or(DecodeFailed::InvalidPretreeRle)? + .iter_mut() + .for_each(|x| *x = 0); + i += zeros as usize + 4; + } + 18 => { + let zeros = bitstream.read_bits(5)?; + self.path_lengths + .get_mut(i..i + zeros as usize + 20) + .ok_or(DecodeFailed::InvalidPretreeRle)? + .iter_mut() + .for_each(|x| *x = 0); + i += zeros as usize + 20; + } + 19 => { + let same = bitstream.read_bits(1)?; + // "Decode new code" is used to parse the next code from the bitstream, which + // has a value range of [0, 16]. + let code = pretree.decode_element(bitstream)?; + if code > 16 { + return Err(DecodeFailed::InvalidPretreeElement(code))?; + } + + let value = (17 + self.path_lengths[i] - code as u8) % 17; + self.path_lengths + .get_mut(i..i + same as usize + 4) + .ok_or(DecodeFailed::InvalidPretreeRle)? + .iter_mut() + .for_each(|x| *x = value); + i += same as usize + 4; + } + _ => return Err(DecodeFailed::InvalidPretreeElement(code)), + }; + } + + Ok(()) + } +} + +impl Tree { + /// Create a new usable tree instance directly from known path lengths. + pub fn from_path_lengths(path_lengths: Vec) -> Result { + CanonicalTree { path_lengths }.create_instance() + } + + pub fn decode_element(&self, bitstream: &mut Bitstream) -> Result { + // Perform the inverse translation, peeking as many bits as our tree is… + let code = self.huffman_tree[bitstream.peek_bits(self.largest_length.get()) as usize]; + + // …and advancing the stream for as many bits this code actually takes (read to seek). + bitstream.read_bits(self.path_lengths[code as usize])?; + + Ok(code) + } +} + +impl fmt::Debug for Tree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Tree") + .field("path_lengths", &self.path_lengths.len()) + .field("largest_length", &self.largest_length) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_simple_table() { + // Based on some aligned offset tree + let tree = Tree::from_path_lengths(vec![6, 5, 1, 3, 4, 6, 2, 0]).unwrap(); + let value_count = vec![(2, 32), (6, 16), (3, 8), (4, 4), (1, 2), (0, 1), (5, 1)]; + + let mut i = 0; + for (value, count) in value_count.into_iter() { + (0..count).for_each(|_| { + assert_eq!(tree.huffman_tree[i], value); + i += 1; + }) + } + } + + #[test] + fn decode_complex_table() { + // Based on the pretree of some length tree + let tree = Tree::from_path_lengths(vec![ + 1, 0, 0, 0, 0, 7, 3, 3, 4, 4, 5, 5, 5, 7, 8, 8, 0, 7, 0, 0, + ]) + .unwrap(); + let value_count = vec![ + (0, 128), + (6, 32), + (7, 32), + (8, 16), + (9, 16), + (10, 8), + (11, 8), + (12, 8), + (5, 2), + (13, 2), + (17, 2), + (14, 1), + (15, 1), + ]; + + let mut i = 0; + for (value, count) in value_count.into_iter() { + (0..count).for_each(|_| { + assert_eq!(tree.huffman_tree[i], value); + i += 1; + }) + } + } + + #[test] + fn decode_elements() { + let tree = Tree::from_path_lengths(vec![6, 5, 1, 3, 4, 6, 2, 0]).unwrap(); + + let buffer = [0x5b, 0xda, 0x3f, 0xf8]; + let mut bitstream = Bitstream::new(&buffer); + bitstream.read_bits(11).unwrap(); + assert_eq!(tree.decode_element(&mut bitstream), Ok(3)); + assert_eq!(tree.decode_element(&mut bitstream), Ok(5)); + assert_eq!(tree.decode_element(&mut bitstream), Ok(6)); + assert_eq!(tree.decode_element(&mut bitstream), Ok(2)); + } +} diff --git a/src/formats/chm/lzx/window.rs b/src/formats/chm/lzx/window.rs new file mode 100644 index 00000000..9dfe70b1 --- /dev/null +++ b/src/formats/chm/lzx/window.rs @@ -0,0 +1,333 @@ +use super::{Bitstream, DecodeFailed, MAX_CHUNK_SIZE}; + +/// The window size is not stored in the compressed data stream and must be known before +/// decoding begins. +/// +/// The window size should be the smallest power of two between 2^17 and 2^25 that is greater +/// than or equal to the sum of the size of the reference data rounded up to a multiple of +/// 32_768 and the size of the subject data. However, some implementations also seem to support +/// a window size of less than 2^17, and this one is no exception. +#[repr(u32)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum WindowSize { + /// Window size of 32 KB (2^15 bytes). + KB32 = 0x0000_8000, + /// Window size of 64 KB (2^16 bytes). + KB64 = 0x0001_0000, + /// Window size of 128 KB (2^17 bytes). + KB128 = 0x0002_0000, + /// Window size of 256 KB (2^18 bytes). + KB256 = 0x0004_0000, + /// Window size of 512 KB (2^19 bytes). + KB512 = 0x0008_0000, + /// Window size of 1 MB (2^20 bytes). + MB1 = 0x0010_0000, + /// Window size of 2 MB (2^21 bytes). + MB2 = 0x0020_0000, +} + +/// A sliding window of a certain size. +/// +/// A `std::collections::VecDeque` is not used because the `deque_make_contiguous` feature +/// is [nightly-only experimental](https://github.com/rust-lang/rust/issues/70929). +pub struct Window { + pos: usize, + buffer: Box<[u8]>, +} + +impl WindowSize { + /// The window size determines the number of window subdivisions, or position slots. + pub(crate) fn position_slots(&self) -> usize { + use WindowSize::*; + + match self { + KB32 => 30, + KB64 => 32, + KB128 => 34, + KB256 => 36, + KB512 => 38, + MB1 => 42, + MB2 => 50, + } + } + + fn value(&self) -> usize { + *self as usize + } + + pub(crate) fn create_buffer(&self) -> Window { + // The window must be at least as big as the smallest chunk, or else we can't possibly + // contain an entire chunk inside of the sliding window. + assert!(self.value() >= MAX_CHUNK_SIZE); + + // We can use bit operations if we rely on this assumption so make sure it holds. + assert!(self.value().is_power_of_two()); + + Window { pos: 0, buffer: vec![0; self.value()].into_boxed_slice() } + } +} + +impl Window { + fn advance(&mut self, delta: usize) { + self.pos += delta; + if self.pos >= self.buffer.len() { + self.pos -= self.buffer.len(); + } + } + + pub fn push(&mut self, value: u8) { + self.buffer[self.pos] = value; + self.advance(1); + } + + pub fn copy_from_self(&mut self, offset: usize, length: usize) { + // For the fast path: + // * Source cannot wrap around + // * `copy_within` won't overwrite as we go but we need that + // * Destination cannot wrap around + if offset <= self.pos && length <= offset && self.pos + length < self.buffer.len() { + // Best case: neither source or destination wrap around + // TODO write a test for this because it used to fail + let start = self.pos - offset; + self.buffer.copy_within(start..start + length, self.pos); + } else { + // Either source or destination wrap around. We could expand this case into three + // (one for only source wrapping, one for only destination wrapping, one for both) + // but it's not really worth the effort. + // + // We could work out the ranges for use in `copy_within` but this is a lot simpler. + let mask = self.buffer.len() - 1; // relying on power of two assumption + + for i in 0..length { + let dst = (self.pos + i) & mask; + let src = (self.buffer.len() + self.pos + i - offset) & mask; + self.buffer[dst] = self.buffer[src]; + } + } + + self.advance(length); + } + + pub fn copy_from_bitstream( + &mut self, + bitstream: &mut Bitstream, + len: usize, + ) -> Result<(), DecodeFailed> { + if len > self.buffer.len() { + return Err(DecodeFailed::WindowTooSmall); + } + + if self.pos + len > self.buffer.len() { + let shift = self.pos + len - self.buffer.len(); + self.pos -= shift; + + // No need to actually save the part we're about to overwrite because when reading + // with the bitstream we would also overwrite it anyway. + self.buffer.copy_within(shift.., 0); + } + + bitstream.read_raw(&mut self.buffer[self.pos..self.pos + len])?; + self.advance(len); + Ok(()) + } + + pub fn past_view(&mut self, len: usize) -> Result<&mut [u8], DecodeFailed> { + if len > MAX_CHUNK_SIZE { + return Err(DecodeFailed::ChunkTooLong); + } + + // Being at zero means we're actually at max length where is impossible for `len` to be + // bigger and we would not want to bother shifting the entire array to end where it was. + if self.pos != 0 && len > self.pos { + let shift = len - self.pos; + self.advance(shift); + + let tmp = self.buffer[self.buffer.len() - shift..].to_vec(); + self.buffer.copy_within(0..self.buffer.len() - shift, shift); + self.buffer[..shift].copy_from_slice(&tmp); + } + + // Because we want to read behind us, being at zero means we're at the end + let pos = if self.pos == 0 { self.buffer.len() } else { self.pos }; + + Ok(&mut self.buffer[pos - len..pos]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_push() { + let mut window = WindowSize::KB32.create_buffer(); + window.push(1); + window.push(2); + window.push(3); + assert_eq!(window.pos, 3); + assert_eq!(&window.buffer[..3], &[1, 2, 3]); + assert!(window.buffer[3..].iter().all(|&x| x == 0)); + } + + #[test] + fn check_push_before_boundary() { + let mut window = WindowSize::KB32.create_buffer(); + window.pos = window.buffer.len() - 1; + window.push(1); + assert_eq!(window.pos, 0); + } + + #[test] + fn check_push_at_boundary() { + let mut window = WindowSize::KB32.create_buffer(); + for _ in 0..((1 << 15) - 2) { + window.push(0); + } + window.push(1); + window.push(2); + window.push(3); + window.push(4); + assert_eq!(window.pos, 2); + assert_eq!(&window.buffer[window.buffer.len() - 2..], &[1, 2]); + assert_eq!(&window.buffer[..2], &[3, 4]); + assert!(window.buffer[2..window.buffer.len() - 2].iter().all(|&x| x == 0)); + } + + #[test] + fn check_copy_from_self() { + let mut window = WindowSize::KB32.create_buffer(); + window.buffer[0] = 1; + window.buffer[1] = 2; + window.buffer[2] = 3; + window.pos = 3; + window.copy_from_self(3, 2); + assert_eq!(window.pos, 5); + assert_eq!(&window.buffer[..5], &[1, 2, 3, 1, 2]); + assert!(window.buffer[5..].iter().all(|&x| x == 0)); + } + + #[test] + fn check_copy_from_self_overlap() { + let mut window = WindowSize::KB32.create_buffer(); + window.buffer[0] = 1; + window.buffer[1] = 2; + window.buffer[2] = 3; + window.pos = 3; + window.copy_from_self(2, 3); + assert_eq!(window.pos, 6); + assert_eq!(&window.buffer[..6], &[1, 2, 3, 2, 3, 2]); + assert!(window.buffer[6..].iter().all(|&x| x == 0)); + } + + #[test] + fn check_copy_at_boundary_from_self() { + let mut window = WindowSize::KB32.create_buffer(); + window.buffer[window.buffer.len() - 3] = 1; + window.buffer[window.buffer.len() - 2] = 2; + window.pos = window.buffer.len() - 1; + window.copy_from_self(2, 2); + assert_eq!(window.pos, 1); + assert_eq!(window.buffer[0], 2); + assert_eq!(&window.buffer[window.buffer.len() - 3..], &[1, 2, 1]); + assert!(window.buffer[1..window.buffer.len() - 3].iter().all(|&x| x == 0)); + } + + #[test] + fn check_copy_from_self_before_boundary() { + let mut window = WindowSize::KB32.create_buffer(); + window.buffer[window.buffer.len() - 4] = 1; + window.buffer[window.buffer.len() - 3] = 2; + window.pos = window.buffer.len() - 2; + window.copy_from_self(2, 2); + assert_eq!(window.pos, 0); + } + + #[test] + fn check_copy_from_self_at_boundary() { + let mut window = WindowSize::KB32.create_buffer(); + window.buffer[window.buffer.len() - 2] = 1; + window.buffer[window.buffer.len() - 1] = 2; + window.buffer[0] = 3; + window.buffer[1] = 4; + window.pos = 2; + window.copy_from_self(4, 3); + assert_eq!(window.pos, 5); + assert_eq!(&window.buffer[..5], &[3, 4, 1, 2, 3]); + assert_eq!(&window.buffer[window.buffer.len() - 2..], &[1, 2]); + assert!(window.buffer[5..window.buffer.len() - 2].iter().all(|&x| x == 0)); + } + + #[test] + fn check_bitstream() { + let buffer = [1, 2, 3, 4]; + let mut bitstream = Bitstream::new(&buffer); + let mut window = WindowSize::KB32.create_buffer(); + window.copy_from_bitstream(&mut bitstream, 4).unwrap(); + assert_eq!(window.pos, 4); + assert_eq!(&window.buffer[..4], &[1, 2, 3, 4]); + assert!(window.buffer[4..].iter().all(|&x| x == 0)); + } + + #[test] + fn check_bitstream_before_boundary() { + let buffer = [1, 2, 3, 4]; + let mut bitstream = Bitstream::new(&buffer); + let mut window = WindowSize::KB32.create_buffer(); + window.pos = window.buffer.len() - 4; + window.copy_from_bitstream(&mut bitstream, 4).unwrap(); + assert_eq!(window.pos, 0); + } + + #[test] + fn check_bitstream_at_boundary() { + let buffer = [1, 2, 3, 4]; + let mut bitstream = Bitstream::new(&buffer); + let mut window = WindowSize::KB32.create_buffer(); + window.pos = window.buffer.len() - 2; + window.copy_from_bitstream(&mut bitstream, 4).unwrap(); + assert_eq!(window.pos, 0); + assert_eq!(&window.buffer[window.buffer.len() - 4..], &[1, 2, 3, 4]); + assert!(window.buffer[..window.buffer.len() - 4].iter().all(|&x| x == 0)); + } + + #[test] + fn check_past_view() { + let mut window = WindowSize::KB32.create_buffer(); + window.buffer[0] = 1; + window.buffer[1] = 2; + window.buffer[2] = 3; + window.pos = 3; + assert_eq!(window.past_view(2).unwrap(), &[2, 3]); + assert_eq!(window.past_view(3).unwrap(), &[1, 2, 3]); + } + + #[test] + fn check_past_view_at_boundary() { + let mut window = WindowSize::KB32.create_buffer(); + window.buffer[window.buffer.len() - 2] = 1; + window.buffer[window.buffer.len() - 1] = 2; + window.buffer[0] = 3; + window.buffer[1] = 4; + window.pos = 2; + assert_eq!(window.past_view(4).unwrap(), &[1, 2, 3, 4]); + } + + #[test] + fn check_past_view_too_long() { + let mut window = WindowSize::KB32.create_buffer(); + assert_eq!(window.past_view(1 << (15 + 1)), Err(DecodeFailed::ChunkTooLong)); + } + + #[test] + fn check_past_view_new_max_size() { + let mut window = WindowSize::KB32.create_buffer(); + assert!(window.past_view(1 << 15).is_ok()); + } + + #[test] + fn check_past_view_shifted_max_size() { + let mut window = WindowSize::KB32.create_buffer(); + window.pos = 123; + assert!(window.past_view(1 << 15).is_ok()); + } +} diff --git a/src/formats/chm/mod.rs b/src/formats/chm/mod.rs new file mode 100644 index 00000000..94884898 --- /dev/null +++ b/src/formats/chm/mod.rs @@ -0,0 +1,789 @@ +//! Compiled HTML Help (.chm): ordered HTML topics in an ITSF container. + +mod archive; +mod html; +mod lzx; + +use crate::error::ConvertError; +use crate::model::{ + AnchorId, Block, Document, ImageSource, Inline, LinkTarget, List, ListItem, MarkerKind, +}; +use crate::package::path::Target; +use crate::package::xml::Element; +use crate::shared::assets::{AssetSink, media_type_for}; +use crate::shared::html::{HtmlCtx, Stylesheet}; +use crate::shared::uri::is_absolute_uri; +use archive::{Archive, Part}; +use encoding_rs::Encoding; +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap, HashSet}; + +pub fn parse(bytes: &[u8]) -> Result { + let archive = Archive::open(bytes)?; + let index = Index::new(archive.entries()); + let lcid = archive.lcid(); + let archive = RefCell::new(archive); + let system = system_info(optional_part(&archive, "#SYSTEM")?.as_deref(), lcid); + let pages: Vec = index.paths.keys().filter(|path| is_html(path)).cloned().collect(); + if pages.is_empty() { + return Err(ConvertError::malformed("CHM contains no HTML topics")); + } + let toc_path = system + .toc + .as_deref() + .and_then(|path| resolve_local("", path, system.encoding, system.compiled.as_deref())) + .and_then(|target| index.canonical(&target.path).map(str::to_string)) + .or_else(|| index.paths.keys().find(|path| extension(path) == "hhc").cloned()); + let mut toc = Vec::new(); + let mut toc_encoding = system.encoding; + let mut toc_base = toc_path.clone().unwrap_or_default(); + if let Some(path) = &toc_path + && let Some(bytes) = optional_part(&archive, index.original(path))? + { + toc_encoding = html::encoding(&bytes, system.encoding); + match html::parse(&bytes, system.encoding) { + Ok(tree) => { + toc_base = + document_base(&tree, &toc_base, toc_encoding, system.compiled.as_deref()); + toc = parse_toc(&tree); + } + Err(e) if e.is_fatal() => return Err(e), + Err(e) => log::warn!("skipping unusable CHM contents {path}: {e}"), + } + } + let assets = RefCell::new(AssetSink::new()); + let mut doc = Document::default(); + let mut ordered = Vec::new(); + let mut seen = HashSet::new(); + collect_topics(&toc, &toc_base, &system, toc_encoding, &index, &mut ordered, &mut seen); + // A missing/binary TOC, or a TOC naming only part of the book, never + // prevents conversion of the remaining HTML. BTreeMap order is stable. + for path in pages { + if seen.insert(path.clone()) { + ordered.push(path); + } + } + let topics: HashSet = ordered.iter().cloned().collect(); + let toc_ctx = Ctx { + archive: &archive, + index: &index, + assets: &assets, + base: toc_path.as_deref().unwrap_or(""), + reference_base: &toc_base, + topics: &topics, + encoding: toc_encoding, + compiled: system.compiled.as_deref(), + }; + if let Some(list) = render_toc(&toc, &toc_ctx) { + doc.blocks.push(list); + } + let mut css_cache = HashMap::new(); + let mut converted = 0usize; + for path in &ordered { + let Some(bytes) = optional_part(&archive, index.original(path))? else { continue }; + let (tree, encoding) = match html::parse_with_encoding(&bytes, system.encoding) { + Ok(parsed) => parsed, + Err(e) if e.is_fatal() => return Err(e), + Err(e) => { + log::warn!("skipping unusable CHM topic {path}: {e}"); + continue; + } + }; + let Some(body) = tree.descendants_any("body").next() else { continue }; + let reference_base = document_base(&tree, path, encoding, system.compiled.as_deref()); + let ctx = Ctx { + archive: &archive, + index: &index, + assets: &assets, + base: path, + reference_base: &reference_base, + topics: &topics, + encoding, + compiled: system.compiled.as_deref(), + }; + let css = stylesheet(&tree, &ctx, &mut css_cache)?; + let blocks = crate::shared::html::to_blocks(body, &css, &ctx)?; + doc.blocks.push(Block::Paragraph(vec![Inline::Anchor(path.clone())])); + if !blocks.iter().any(|block| matches!(block, Block::Heading { level: 1, .. })) + && let Some(title) = tree.descendants_any("title").next().map(Element::text) + && !title.trim().is_empty() + { + doc.blocks.push(Block::heading(1, vec![Inline::plain(title.trim())])); + } + doc.blocks.extend(blocks); + converted += 1; + } + if converted == 0 { + return Err(ConvertError::malformed("no HTML topic in the CHM could be read")); + } + doc.assets = std::mem::take(&mut assets.borrow_mut().assets); + Ok(doc) +} + +fn optional_part<'a>( + archive: &RefCell>, + path: &str, +) -> Result>, ConvertError> { + match archive.borrow_mut().part(path) { + Ok(bytes) => Ok(bytes), + Err(e) if e.is_fatal() => Err(e), + Err(e) => { + log::warn!("skipping unreadable CHM part {path}: {e}"); + Ok(None) + } + } +} + +struct Index { + paths: BTreeMap, + folded: HashMap, +} + +impl Index { + fn new<'a>(entries: impl Iterator) -> Self { + let mut paths = BTreeMap::new(); + for original in entries { + // Directory names are already decoded by the archive. Percent + // escapes belong to hrefs, not to the stored names themselves. + let path = normalize_path(original); + paths.entry(path).or_insert_with(|| original.to_string()); + } + let mut folded = HashMap::new(); + for path in paths.keys() { + folded.entry(path.to_ascii_lowercase()).or_insert_with(|| path.clone()); + } + Self { paths, folded } + } + + fn canonical(&self, path: &str) -> Option<&str> { + self.paths + .get_key_value(path) + .map(|(key, _)| key.as_str()) + .or_else(|| self.folded.get(&path.to_ascii_lowercase()).map(String::as_str)) + } + + fn original<'a>(&'a self, path: &'a str) -> &'a str { + self.paths.get(path).map(String::as_str).unwrap_or(path) + } +} + +fn normalize_path(path: &str) -> String { + let mut parts = Vec::new(); + for part in path.split(['/', '\\']) { + match part { + "" | "." => {} + ".." => { + parts.pop(); + } + part => parts.push(part), + } + } + parts.join("/") +} + +fn extension(path: &str) -> String { + path.rsplit_once('.').map(|(_, ext)| ext.to_ascii_lowercase()).unwrap_or_default() +} + +fn is_html(path: &str) -> bool { + !matches!(path.chars().next(), Some('#' | '$' | ':')) + && matches!(extension(path).as_str(), "htm" | "html" | "xhtml" | "shtml") +} + +struct System { + toc: Option, + compiled: Option, + encoding: &'static Encoding, +} + +fn system_info(bytes: Option<&[u8]>, lcid: u32) -> System { + let mut records = Vec::new(); + let mut lcid = lcid; + if let Some(bytes) = bytes { + let mut pos = 4usize; // DWORD version, then WORD code/WORD length records. + while let Some(header) = bytes.get(pos..pos.saturating_add(4)) { + let code = u16::from_le_bytes([header[0], header[1]]); + let len = u16::from_le_bytes([header[2], header[3]]) as usize; + pos += 4; + let Some(value) = bytes.get(pos..pos.saturating_add(len)) else { + log::warn!("ignoring a truncated CHM #SYSTEM record"); + break; + }; + if code == 4 && value.len() >= 4 { + lcid = u32::from_le_bytes(value[..4].try_into().unwrap()); + } + records.push((code, value)); + pos += len; + } + } + let encoding = language_encoding(lcid); + let string = |code| { + records.iter().find(|(kind, _)| *kind == code).and_then(|(_, value)| { + let value = value.split(|b| *b == 0).next().unwrap_or(value); + let text = html::decode(value, encoding); + (!text.trim().is_empty()).then(|| text.trim().to_string()) + }) + }; + System { toc: string(0), compiled: string(6), encoding } +} + +fn language_encoding(lcid: u32) -> &'static Encoding { + // Script variants have different ANSI codepages even with the same + // primary language. Other regional variants share their family's page. + match lcid & 0xffff { + 0x0404 | 0x0c04 | 0x1404 => return encoding_rs::BIG5, + 0x082c | 0x0843 | 0x0c1a | 0x1c1a | 0x201a | 0x281a | 0x301a => { + return encoding_rs::WINDOWS_1251; + } + _ => {} + } + match lcid & 0x03ff { + 0x04 => encoding_rs::GBK, + 0x11 => encoding_rs::SHIFT_JIS, + 0x12 => encoding_rs::EUC_KR, + 0x05 | 0x0e | 0x15 | 0x18 | 0x1a | 0x1b | 0x1c | 0x24 => encoding_rs::WINDOWS_1250, + 0x02 | 0x19 | 0x22 | 0x23 | 0x28 | 0x2f | 0x3f | 0x40 | 0x44 | 0x50 => { + encoding_rs::WINDOWS_1251 + } + 0x08 => encoding_rs::WINDOWS_1253, + 0x1f | 0x2c | 0x43 => encoding_rs::WINDOWS_1254, + 0x0d => encoding_rs::WINDOWS_1255, + 0x01 | 0x20 | 0x29 | 0x63 | 0x80 => encoding_rs::WINDOWS_1256, + 0x25..=0x27 => encoding_rs::WINDOWS_1257, + 0x2a => encoding_rs::WINDOWS_1258, + 0x1e => encoding_rs::WINDOWS_874, + _ => encoding_rs::WINDOWS_1252, + } +} + +#[derive(Debug)] +struct TocNode { + title: String, + href: Option, + children: Vec, +} + +fn parse_toc(tree: &Element) -> Vec { + tree.descendants_any("ul").next().map(toc_list).unwrap_or_default() +} + +fn toc_list(list: &Element) -> Vec { + let mut nodes: Vec = Vec::new(); + for child in list.child_elems() { + if child.local == "ul" { + let nested = toc_list(child); + if let Some(previous) = nodes.last_mut() { + previous.children.extend(nested); + } else { + nodes.extend(nested); + } + continue; + } + if child.local != "li" { + continue; + } + let object = child.child_elems().find(|elem| { + elem.local == "object" + && elem + .attr_any("type") + .is_some_and(|kind| kind.eq_ignore_ascii_case("text/sitemap")) + }); + let mut title = String::new(); + let mut href = None; + if let Some(object) = object { + for param in object.descendants_any("param") { + let value = param.attr_any("value").unwrap_or("").trim(); + match param.attr_any("name").unwrap_or("").to_ascii_lowercase().as_str() { + "name" => title = value.to_string(), + "local" if !value.is_empty() => href = Some(value.to_string()), + _ => {} + } + } + } + // Some producers emit ordinary linked HTML rather than sitemap objects. + if object.is_none() + && let Some(link) = child.child_elems().find(|elem| elem.local == "a") + { + title = link.text().trim().to_string(); + href = link.attr_any("href").map(str::to_string); + } + let children: Vec = + child.child_elems().filter(|elem| elem.local == "ul").flat_map(toc_list).collect(); + if !title.is_empty() || href.is_some() { + nodes.push(TocNode { title, href, children }); + } else { + nodes.extend(children); + } + } + nodes +} + +fn collect_topics( + nodes: &[TocNode], + base: &str, + system: &System, + encoding: &'static Encoding, + index: &Index, + ordered: &mut Vec, + seen: &mut HashSet, +) { + for node in nodes { + if let Some(href) = &node.href + && let Some(target) = resolve_target(base, href, encoding, system.compiled.as_deref()) + && let Some(path) = index.canonical(&target.path).filter(|path| is_html(path)) + && seen.insert(path.to_string()) + { + ordered.push(path.to_string()); + } + collect_topics(&node.children, base, system, encoding, index, ordered, seen); + } +} + +fn render_toc(nodes: &[TocNode], ctx: &Ctx<'_, '_>) -> Option { + let items: Vec = nodes + .iter() + .map(|node| { + let label = if node.title.is_empty() { + node.href.as_deref().unwrap_or("") + } else { + &node.title + }; + let content = vec![Inline::plain(label)]; + let content = match node.href.as_deref().and_then(|href| ctx.link_target(href)) { + Some(target) => vec![Inline::Link { content, target }], + None => content, + }; + let mut blocks = vec![Block::Paragraph(content)]; + if let Some(children) = render_toc(&node.children, ctx) { + blocks.push(children); + } + ListItem { blocks, marker_label: None } + }) + .collect(); + (!items.is_empty()).then_some(Block::List(List { marker: MarkerKind::Bullet, start: 1, items })) +} + +fn stylesheet( + tree: &Element, + ctx: &Ctx<'_, '_>, + cache: &mut HashMap<(String, &'static str), Option>, +) -> Result { + let mut sheet = Stylesheet::default(); + for elem in tree.descendant_elems() { + if elem.local == "style" { + sheet.add(&elem.text()); + } else if elem.local == "link" + && elem + .attr_any("rel") + .unwrap_or("") + .split_whitespace() + .any(|rel| rel.eq_ignore_ascii_case("stylesheet")) + && let Some(href) = elem.attr_any("href") + && let Some(target) = ctx.resolve(href) + && let Some(path) = ctx.index.canonical(&target.path) + { + // A stylesheet without its own charset inherits the referring + // page's encoding, so the decoded cache must include that context. + let key = (path.to_string(), ctx.encoding.name()); + if !cache.contains_key(&key) { + let text = optional_part(ctx.archive, ctx.index.original(path))? + .map(|bytes| html::decode_stylesheet(&bytes, ctx.encoding)); + cache.insert(key.clone(), text); + } + if let Some(Some(text)) = cache.get(&key) { + sheet.add(text); + } + } + } + Ok(sheet) +} + +struct Ctx<'a, 'b> { + archive: &'b RefCell>, + index: &'b Index, + assets: &'b RefCell, + base: &'b str, + reference_base: &'b str, + topics: &'b HashSet, + encoding: &'static Encoding, + compiled: Option<&'b str>, +} + +impl Ctx<'_, '_> { + fn resolve(&self, reference: &str) -> Option { + resolve_target(self.reference_base, reference, self.encoding, self.compiled) + } +} + +impl HtmlCtx for Ctx<'_, '_> { + fn link_target(&self, href: &str) -> Option { + if href.trim().is_empty() { + return None; + } + if let Some(target) = self.resolve(href) + && let Some(path) = + self.index.canonical(&target.path).filter(|path| self.topics.contains(*path)) + { + return Some(LinkTarget::Anchor(scoped(path, target.fragment.as_deref()))); + } + let href = resolve_reference(self.reference_base, href); + Some(if is_absolute_uri(&href) || href.starts_with("//") { + LinkTarget::External(href) + } else { + LinkTarget::Relative(href) + }) + } + + fn image_source(&self, src: &str) -> Result, ConvertError> { + if src.trim().is_empty() { + return Ok(None); + } + if let Some(target) = self.resolve(src) { + let Some(path) = self.index.canonical(&target.path) else { return Ok(None) }; + return match optional_part(self.archive, self.index.original(path))? { + Some(bytes) => self + .assets + .borrow_mut() + .add(media_type_for(path), path.to_string(), &bytes) + .map(|id| Some(ImageSource::Asset(id))), + None => Ok(None), + }; + } + let src = resolve_reference(self.reference_base, src); + Ok((is_absolute_uri(&src) || src.starts_with("//")).then_some(ImageSource::External(src))) + } + + fn anchor_id(&self, raw: &str) -> AnchorId { + scoped(self.base, Some(raw)) + } +} + +fn scoped(path: &str, fragment: Option<&str>) -> AnchorId { + match fragment.filter(|fragment| !fragment.is_empty()) { + Some(fragment) => format!("{path}#{fragment}"), + None => path.to_string(), + } +} + +/// The first base href controls references, while anchor definitions always +/// stay scoped to the actual topic path in Ctx::base. +fn document_base( + tree: &Element, + path: &str, + encoding: &'static Encoding, + compiled: Option<&str>, +) -> String { + let Some(href) = tree.descendants_any("base").find_map(|elem| elem.attr_any("href")) else { + return path.to_string(); + }; + let href = href.trim(); + if let Some(mut target) = resolve_local(path, href, encoding, compiled) { + let raw_path = href.split(['?', '#']).next().unwrap_or(href); + if raw_path.ends_with(['/', '\\']) + || matches!(raw_path.rsplit('/').next(), Some("." | "..")) + { + target.path.push('/'); + } + return target.path; + } + if (is_absolute_uri(href) || href.starts_with("//")) + && !href.to_ascii_lowercase().starts_with("javascript:") + && !href.to_ascii_lowercase().starts_with("data:") + { + return href.to_string(); + } + path.to_string() +} + +fn resolve_target( + base: &str, + reference: &str, + encoding: &'static Encoding, + compiled: Option<&str>, +) -> Option { + let reference = resolve_reference(base, reference); + if (is_absolute_uri(base) || base.starts_with("//")) + && !is_absolute_uri(&reference) + && !reference.starts_with("::") + { + return None; + } + resolve_local(base, &reference, encoding, compiled) +} + +/// Join an external base without decoding its percent-escaped path. Local +/// CHM references instead retain the legacy encoding and archive checks. +fn resolve_reference(base: &str, reference: &str) -> String { + let reference = reference.trim(); + if is_absolute_uri(reference) || reference.starts_with("::") { + return reference.to_string(); + } + let hierarchy = + base.find("://").map(|pos| pos + 3).or_else(|| base.starts_with("//").then_some(2)); + let (origin, path) = if let Some(start) = hierarchy { + if reference.starts_with("//") { + return base.find("://").map_or_else( + || reference.to_string(), + |pos| format!("{}{reference}", &base[..=pos]), + ); + } + let end = base[start..].find(['/', '?', '#']).map_or(base.len(), |pos| start + pos); + (&base[..end], &base[end..]) + } else if is_absolute_uri(base) + && let Some((origin, path)) = base.split_once("::") + { + (&base[..origin.len() + 2], path) + } else { + return reference.to_string(); + }; + if reference.starts_with('#') { + return format!("{origin}{}{reference}", path.split('#').next().unwrap_or(path)); + } + if reference.starts_with('?') { + return format!("{origin}{}{reference}", path.split(['?', '#']).next().unwrap_or(path)); + } + let base_path = path.split(['?', '#']).next().unwrap_or(path); + let split = reference.find(['?', '#']).unwrap_or(reference.len()); + let (relative_path, suffix) = reference.split_at(split); + if relative_path.is_empty() { + return format!("{origin}{path}"); + } + let joined = if relative_path.starts_with('/') { + relative_path.to_string() + } else { + let directory = base_path.rsplit_once('/').map_or("", |(dir, _)| dir); + format!("{directory}/{relative_path}") + }; + let trailing = joined.ends_with('/') || matches!(joined.rsplit('/').next(), Some("." | "..")); + let mut normalized = format!("/{}", normalize_path(&joined)); + if trailing && !normalized.ends_with('/') { + normalized.push('/'); + } + format!("{origin}{normalized}{suffix}") +} + +fn resolve_local( + base: &str, + reference: &str, + encoding: &'static Encoding, + compiled: Option<&str>, +) -> Option { + let mut reference = reference.trim(); + let mut archive_root = false; + let lower = reference.to_ascii_lowercase(); + if lower.starts_with("ms-its:") + || lower.starts_with("its:") + || lower.starts_with("mk:@msitstore:") + { + let (book, topic) = reference.split_once("::")?; + let book = book.split_once(':')?.1.trim_start_matches('@'); + let book = book.rsplit(['/', '\\', ':']).next().unwrap_or(book); + let compiled = compiled?; + let stem = |name: &str| { + let name = name.to_ascii_lowercase(); + name.strip_suffix(".chm").unwrap_or(&name).to_string() + }; + if stem(book) != stem(compiled) { + return None; + } + reference = topic.split('>').next().unwrap_or(topic); + archive_root = true; + } else if let Some(topic) = reference.strip_prefix("::") { + reference = topic; + archive_root = true; + } else if is_absolute_uri(reference) || reference.starts_with("//") { + return None; + } + let (path, fragment) = match reference.split_once('#') { + Some((path, fragment)) => (path, Some(decode_url(fragment, encoding))), + None => (reference, None), + }; + let path = path.split('?').next().unwrap_or(path).replace('\\', "/"); + if path.is_empty() { + return Some(Target { path: base.to_string(), fragment }); + } + let mut parts: Vec = if archive_root || path.starts_with('/') { + Vec::new() + } else { + base.rsplit_once('/') + .map(|(dir, _)| { + dir.split('/').filter(|part| !part.is_empty()).map(str::to_string).collect() + }) + .unwrap_or_default() + }; + for part in path.split('/') { + match part { + "" | "." => {} + ".." => { + parts.pop(); + } + part => { + let decoded = decode_url(part, encoding); + if decoded.contains(['/', '\\']) || decoded == "." || decoded == ".." { + return None; + } + parts.push(decoded); + } + } + } + Some(Target { path: parts.join("/"), fragment }) +} + +fn decode_url(component: &str, encoding: &'static Encoding) -> String { + let bytes = component.as_bytes(); + let mut decoded = String::with_capacity(bytes.len()); + let mut pending = Vec::new(); + let mut pos = 0; + while pos < bytes.len() { + if bytes[pos] == b'%' + && let Some(pair) = bytes.get(pos + 1..pos + 3) + && pair.iter().all(u8::is_ascii_hexdigit) + { + pending.push(u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()); + pos += 3; + } else { + let ch = component[pos..].chars().next().unwrap(); + if ch.is_ascii() { + // A literal ASCII byte can be a legacy multibyte trail, as + // in GBK %81A. Decode it with the adjacent escaped bytes. + pending.push(ch as u8); + } else { + decoded.push_str(&encoding.decode(&pending).0); + pending.clear(); + decoded.push(ch); + } + pos += ch.len_utf8(); + } + } + decoded.push_str(&encoding.decode(&pending).0); + decoded +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn regional_lcids_use_their_legacy_windows_codepage() { + for (lcid, expected) in [ + (0x0415, encoding_rs::WINDOWS_1250), + (0x0408, encoding_rs::WINDOWS_1253), + (0x041f, encoding_rs::WINDOWS_1254), + (0x040d, encoding_rs::WINDOWS_1255), + (0x0c01, encoding_rs::WINDOWS_1256), + (0x0425, encoding_rs::WINDOWS_1257), + (0x042a, encoding_rs::WINDOWS_1258), + (0x041e, encoding_rs::WINDOWS_874), + (0x081a, encoding_rs::WINDOWS_1250), + (0x0c1a, encoding_rs::WINDOWS_1251), + (0x082c, encoding_rs::WINDOWS_1251), + ] { + assert_eq!(system_info(None, lcid).encoding, expected, "LCID {lcid:#06x}"); + } + } + + #[test] + fn legacy_percent_escapes_keep_the_selected_encoding_and_literal_unicode() { + assert_eq!(decode_url("%C2%A9", encoding_rs::GBK), "漏"); + assert_eq!(decode_url("漏%C2%A9", encoding_rs::GBK), "漏漏"); + assert_eq!(decode_url("%C2%A9", encoding_rs::UTF_8), "©"); + assert_eq!(decode_url("%81A", encoding_rs::GBK), "丄"); + assert_eq!(decode_url("乙%A4A", encoding_rs::BIG5), "乙乙"); + } + + #[test] + fn external_bases_keep_authority_query_and_fragment_semantics() { + for (base, reference, expected) in [ + ( + "https://example.invalid/a/page.htm?old#old", + "../b.htm?q#f", + "https://example.invalid/b.htm?q#f", + ), + ( + "https://example.invalid/a/page.htm?old#old", + "#new", + "https://example.invalid/a/page.htm?old#new", + ), + ( + "https://example.invalid/a/page.htm?old", + "?new#f", + "https://example.invalid/a/page.htm?new#f", + ), + ("https://example.invalid/a/", "//cdn.invalid/p.png", "https://cdn.invalid/p.png"), + ("//example.invalid:8080/a/", "//cdn.invalid/p.png", "//cdn.invalid/p.png"), + ("https://example.invalid", "p.png", "https://example.invalid/p.png"), + ] { + assert_eq!(resolve_reference(base, reference), expected); + } + } + + #[test] + fn hhc_preserves_nested_groups_and_repeated_topic_references() { + let tree = html::parse( + br#"
  • +
    • +
    +
"#, + encoding_rs::UTF_8, + ) + .unwrap(); + let nodes = parse_toc(&tree); + assert_eq!(nodes.len(), 2); + assert_eq!(nodes[0].title, "Group"); + assert!(nodes[0].href.is_none()); + assert_eq!(nodes[0].children.len(), 2); + assert_eq!(nodes[0].children[1].href.as_deref(), Some("a.htm#syntax")); + assert_eq!(nodes[1].href.as_deref(), Some("z.htm")); + } + + #[test] + fn local_references_decode_legacy_urls_and_keep_encoded_structure_rejected() { + let target = resolve_local( + "docs/start.htm", + "../%C5%E4%D6%C3.htm#%B2%CE%CA%FD", + encoding_rs::GBK, + None, + ) + .unwrap(); + assert_eq!(target.path, "配置.htm"); + assert_eq!(target.fragment.as_deref(), Some("参数")); + assert_eq!( + resolve_local("docs/start.htm", r"..\images\pic.png", encoding_rs::UTF_8, None) + .unwrap() + .path, + "images/pic.png" + ); + assert!(resolve_local("", "%2e%2e/escape.htm", encoding_rs::UTF_8, None).is_none()); + assert!(resolve_local("", "a%2fb.htm", encoding_rs::UTF_8, None).is_none()); + } + + #[test] + fn chm_wrapper_links_are_local_only_for_the_same_book() { + let target = resolve_local( + "", + "ms-its:manual.chm::/topic.htm#api>main", + encoding_rs::UTF_8, + Some("manual"), + ) + .unwrap(); + assert_eq!(target.path, "topic.htm"); + assert_eq!(target.fragment.as_deref(), Some("api")); + for reference in ["ms-its:manual.chm::topic.htm#api", "::topic.htm#api"] { + assert_eq!( + resolve_local("docs/start.htm", reference, encoding_rs::UTF_8, Some("manual")) + .unwrap() + .path, + "topic.htm" + ); + } + assert!( + resolve_local( + "", + "mk:@MSITStore:other.chm::/topic.htm", + encoding_rs::UTF_8, + Some("manual") + ) + .is_none() + ); + } +} diff --git a/src/formats/detect.rs b/src/formats/detect.rs index 727a69f6..faf17429 100644 --- a/src/formats/detect.rs +++ b/src/formats/detect.rs @@ -3,6 +3,7 @@ //! Identifies the format from what each specification designates as the //! container's identity, never from heuristics over document content: //! +//! - CHM: the `ITSF` container signature. //! - PDF: the `%PDF-` header (ISO 32000; implementations accept leading //! junk, bounded here at 1024 bytes). //! - RTF: the `{\rtf` group that must open every RTF file. @@ -32,6 +33,9 @@ const MANIFEST_NS: &str = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"; const SPREADSHEET_NS: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"; pub(crate) fn from_bytes(bytes: &[u8]) -> Option { + if bytes.starts_with(b"ITSF") { + return Some(Format::Chm); + } if bytes.starts_with(b"{\\rtf") { return Some(Format::Rtf); } diff --git a/src/formats/mod.rs b/src/formats/mod.rs index d570b264..2bc5b024 100644 --- a/src/formats/mod.rs +++ b/src/formats/mod.rs @@ -1,5 +1,6 @@ //! One frontend per input format; each parses bytes into the document model. +mod chm; mod csv; pub mod detect; mod doc; @@ -18,6 +19,7 @@ use crate::model::Document; pub fn parse(bytes: &[u8], format: Format) -> Result { match format { + Format::Chm => chm::parse(bytes), Format::Excel => sheet::parse(bytes), Format::Csv => csv::parse(bytes), Format::Docx => docx::parse(bytes), diff --git a/src/lib.rs b/src/lib.rs index 4ca94c58..505ba1fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,8 @@ use std::path::Path; /// [`Format::from_extension`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Format { + /// Compiled HTML Help (`.chm`). + Chm, /// Binary Word 97-2003 (`.doc`). Doc, /// WordprocessingML (`.docx`, `.docm`), both Transitional and Strict. @@ -72,6 +74,7 @@ impl Format { /// case-insensitively. `None` for anything unrecognized. pub fn from_extension(ext: &str) -> Option { Some(match ext.to_ascii_lowercase().as_str() { + "chm" => Format::Chm, "doc" => Format::Doc, "docx" | "docm" => Format::Docx, "odt" => Format::Odt, diff --git a/src/render/markdown/mod.rs b/src/render/markdown/mod.rs index 095c10e0..ea4093c2 100644 --- a/src/render/markdown/mod.rs +++ b/src/render/markdown/mod.rs @@ -240,7 +240,8 @@ fn render_list(list: &List, rc: &Ctx) -> Option { (None, kind) => format!("- {} ", kind.label(list.start.saturating_add(i as u64))), }; let body = render_blocks(&item.blocks, rc); - if item.blocks.len() > 1 { + // Invisible anchor paragraphs must not turn a tight list loose. + if item.blocks.iter().filter(|block| !block_is_blank(block)).take(2).count() > 1 { loose = true; } let indent = " ".repeat(marker.chars().count()); diff --git a/src/shared/html.rs b/src/shared/html.rs index 3a7590b6..24932511 100644 --- a/src/shared/html.rs +++ b/src/shared/html.rs @@ -34,8 +34,12 @@ pub fn to_blocks( ) -> Result, ConvertError> { let mut builder = Builder { blocks: Vec::new(), inlines: Vec::new(), css, ctx, start_boundary: true }; + builder.push_anchor(body); + let anchors = std::mem::take(&mut builder.inlines); builder.walk_children(body, StyleDelta::default())?; - Ok(builder.finish()) + let mut blocks = builder.finish(); + prepend_anchors(&mut blocks, anchors); + Ok(blocks) } // --------------------------------------------------------------------------- @@ -224,6 +228,36 @@ fn keeps_paragraph(inlines: &[Inline]) -> bool { !inlines_are_empty(inlines) || inlines.iter().any(|i| matches!(i, Inline::Anchor(_))) } +/// Owner-element anchors occupy the start of their content. Merge them into +/// the first inline-bearing block so invisible ids do not create extra list +/// paragraphs or table-cell line breaks. +fn prepend_anchors(blocks: &mut Vec, mut anchors: Vec) { + if anchors.is_empty() { + return; + } + match blocks.first_mut() { + Some(Block::Paragraph(content) | Block::Heading { content, .. }) => { + anchors.append(content); + *content = anchors; + } + Some(Block::BlockQuote(inner)) => prepend_anchors(inner, anchors), + Some(Block::List(list)) if !list.items.is_empty() => { + prepend_anchors(&mut list.items[0].blocks, anchors); + } + Some(Block::Table(table)) => { + if let Some(cell) = table.grid.iter_mut().flatten().find_map(|slot| match slot { + crate::model::CellSlot::Origin(cell) => Some(cell), + _ => None, + }) { + prepend_anchors(&mut cell.blocks, anchors); + } else { + blocks.insert(0, Block::Paragraph(anchors)); + } + } + _ => blocks.insert(0, Block::Paragraph(anchors)), + } +} + /// Whether text appended after `inlines` sits at a whitespace boundary /// (its leading spaces collapse away): at a block start, after emitted /// whitespace or a line break, looking through zero-width anchors. @@ -274,7 +308,7 @@ impl Builder<'_> { elem: &Element, delta: StyleDelta, ) -> Result, ConvertError> { - self.sub_blocks_at(elem, delta, true) + self.sub_blocks_at(elem, delta, true, true) } /// Sub-walk starting at the given whitespace-boundary state (`false` @@ -285,6 +319,7 @@ impl Builder<'_> { elem: &Element, delta: StyleDelta, start_boundary: bool, + anchor_self: bool, ) -> Result, ConvertError> { let mut b = Builder { blocks: Vec::new(), @@ -293,8 +328,14 @@ impl Builder<'_> { ctx: self.ctx, start_boundary, }; + if anchor_self { + b.push_anchor(elem); + } + let anchors = std::mem::take(&mut b.inlines); b.walk_children(elem, delta)?; - Ok(b.finish()) + let mut blocks = b.finish(); + prepend_anchors(&mut blocks, anchors); + Ok(blocks) } /// Element-level props: matching stylesheet rules and the inline `style` @@ -400,11 +441,15 @@ impl Builder<'_> { } } "ul" | "ol" => { + self.flush_paragraph(); + self.push_anchor(elem); self.flush_paragraph(); let lists = self.parse_list(elem, delta)?; self.blocks.extend(lists); } "table" => { + self.flush_paragraph(); + self.push_anchor(elem); self.flush_paragraph(); if let Some(caption) = elem.child_elems().find(|e| e.local == "caption") { let content = self.inline_children(caption, delta)?; @@ -424,6 +469,8 @@ impl Builder<'_> { } } "pre" => { + self.flush_paragraph(); + self.push_anchor(elem); self.flush_paragraph(); let text = elem.text(); if !text.trim().is_empty() { @@ -431,10 +478,13 @@ impl Builder<'_> { } } "hr" => { + self.flush_paragraph(); + self.push_anchor(elem); self.flush_paragraph(); self.blocks.push(Block::Rule); } "math" => { + self.push_anchor(elem); let tex = mathml_to_tex(elem); if tex.is_empty() { } else if mathml_is_display(elem) { @@ -509,7 +559,7 @@ impl Builder<'_> { delta: StyleDelta, start_boundary: bool, ) -> Result, ConvertError> { - let mut blocks = self.sub_blocks_at(elem, delta, start_boundary)?; + let mut blocks = self.sub_blocks_at(elem, delta, start_boundary, false)?; if blocks.len() == 1 && let Block::Paragraph(inlines) = &mut blocks[0] { @@ -658,6 +708,7 @@ impl Builder<'_> { if !matches!(cell.local.as_str(), "td" | "th") { continue; } + let first_cell = !any_cell; any_cell = true; if cell.local != "th" { all_th = false; @@ -674,7 +725,10 @@ impl Builder<'_> { Some(n) => n.clamp(1, 65534), None => 1, }; - let blocks = self.sub_blocks(cell, delta)?; + let mut blocks = self.sub_blocks(cell, delta)?; + if first_cell && let Some(id) = tr.attr_any("id").filter(|id| !id.is_empty()) { + prepend_anchors(&mut blocks, vec![Inline::Anchor(self.ctx.anchor_id(id))]); + } builder.place(Cell::spanning(blocks, col_span, row_span))?; } if i == header_rows && (*in_head || (all_th && any_cell)) { @@ -783,6 +837,49 @@ mod tests { use crate::model::{CellSlot, ImageSource, Style}; use crate::package::xml::parse_xml; + #[test] + fn structural_html_element_ids_remain_internal_link_targets() { + let tree = parse_xml( + br#"
  • Item
+
Cell
+
literal

"#, + ) + .unwrap(); + let mut blocks = + to_blocks(tree.child_elems().next().unwrap(), &Stylesheet::default(), &NullCtx) + .unwrap(); + let ids = ["body", "list", "item", "table", "row", "cell", "code", "rule"]; + blocks.push(Block::Paragraph( + ids.iter() + .map(|id| Inline::Link { + content: vec![Inline::plain(*id)], + target: LinkTarget::Anchor((*id).into()), + }) + .collect(), + )); + let doc = crate::model::Document { blocks, ..Default::default() }; + let markdown = crate::render::markdown::document_to_markdown(&doc); + for id in ids { + assert!(markdown.contains(&format!("[{id}](#{id})")), "lost {id}: {markdown}"); + } + } + + #[test] + fn unreferenced_structural_anchors_preserve_tight_list_spacing() { + let render = |html: &str| { + let doc = crate::model::Document { blocks: blocks(html), ..Default::default() }; + crate::render::markdown::document_to_markdown(&doc) + }; + let plain = render("
  • A

  • B

"); + let anchored = render("
  • A

  • B

"); + assert_eq!(plain, "- A\n- B\n"); + assert_eq!(anchored, plain); + let plain = render("
  • A
  • B
"); + let anchored = + render("
  • A
  • B
"); + assert_eq!(anchored, plain); + } + struct NullCtx; impl HtmlCtx for NullCtx { diff --git a/tests/chm.rs b/tests/chm.rs new file mode 100644 index 00000000..0fc940b9 --- /dev/null +++ b/tests/chm.rs @@ -0,0 +1,252 @@ +//! CHM conversion contracts. The vendor manual stays outside the fixture corpus. + +use anydoc::model::{Block, Document, ImageSource, Inline, LinkTarget, inlines_to_plain_text}; +use anydoc::{ConvertError, Format, to_document, to_markdown, to_markdown_bytes}; +use std::path::Path; + +fn format() -> Format { + Format::from_extension("chm").expect("CHM must be a supported input format") +} + +fn fixture() -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/chm/handmade-book.chm") +} + +#[test] +fn chm_is_identified_by_content_and_extension() { + let chm = format(); + assert_eq!(Format::from_extension("CHM"), Some(chm)); + assert_eq!(Format::from_path(Path::new("manual.chm")), Some(chm)); + assert_eq!(Format::from_bytes(b"ITSF"), Some(chm)); + assert_eq!(Format::from_bytes(b"ITS"), None); +} + +#[test] +fn chm_preserves_topics_links_tables_and_assets() { + let bytes = std::fs::read(fixture()).expect("committed CHM fixture"); + let md = to_markdown_bytes(&bytes, format()).expect("convert CHM bytes"); + assert_eq!(md, to_markdown_bytes(&bytes, None).unwrap()); + assert_eq!(md, to_markdown(fixture()).unwrap()); + assert!(md.contains("中文说明:配置路由器"), "{md}"); + assert!(md.contains("show version"), "{md}"); + assert!(md.contains("| 命令 | 说明 |"), "{md}"); + assert!(md.contains("[命令语法](#"), "{md}"); + assert!(!md.contains("](a-commands.htm"), "topic links must become anchors: {md}"); + assert!(md.contains("拓扑图"), "{md}"); + let intro = md.find("# 使用指南").expect("intro heading"); + let commands = md.find("命令参考\n").expect("commands heading"); + let extra = md.find("补充说明\n").expect("unlisted topic must survive"); + assert!(intro < commands && commands < extra, "TOC order followed by unlisted topics: {md}"); + assert_eq!( + md.matches("中文说明:配置路由器").count(), + 1, + "duplicate TOC entries duplicate no body" + ); + let doc = to_document(&bytes, None).expect("CHM has a document model"); + assert_eq!(doc.assets.len(), 1, "shared image is retained once"); + assert_eq!(doc.assets[0].media_type, "image/png"); + assert_eq!(doc.assets[0].bytes, include_bytes!("fixture-src/dot.png")); +} + +#[test] +fn truncated_chm_returns_a_typed_error() { + assert!(matches!( + to_markdown_bytes(b"ITSF\x03\0\0\0", format()), + Err(ConvertError::Malformed { .. } | ConvertError::MissingPart { .. }) + )); +} + +#[test] +fn chm_without_a_toc_has_a_stable_order() { + let path = fixture().with_file_name("handmade-no-toc.chm"); + let md = to_markdown(path).unwrap(); + assert!(md.find("# First").unwrap() < md.find("# Second").unwrap(), "{md}"); +} + +#[test] +fn chm_decodes_big5_topics() { + let path = fixture().with_file_name("handmade-big5.chm"); + let md = to_markdown(path).unwrap(); + assert!(md.contains("繁體中文"), "{md}"); + assert!(md.contains("路由器設定與命令參考"), "{md}"); + assert!(md.contains("[參數連結](#參數)"), "links use the topic encoding: {md}"); + assert!(!md.contains('\u{fffd}')); +} + +#[test] +fn compressed_and_stored_chm_have_the_same_document_content() { + let stored = to_markdown(fixture()).unwrap(); + let compressed = to_markdown(fixture().with_file_name("handmade-compressed.chm")).unwrap(); + assert_eq!(compressed, stored); +} + +#[test] +fn huffman_spaninfo_decodes_frames_across_dictionary_resets() { + let bytes = std::fs::read(fixture().with_file_name("handmade-huffman-spaninfo.chm")).unwrap(); + assert!(bytes.len() < 10_000, "the original Huffman fixture stays small"); + let md = to_markdown_bytes(&bytes, None).expect("decode Huffman frames without ResetTable"); + assert_eq!(md.trim().len(), 131_072); + assert_eq!(md.trim(), format!("{}{}", "A".repeat(65_536), "B".repeat(65_536))); +} + +fn regression_document(name: &str) -> Document { + let bytes = std::fs::read(fixture().with_file_name(name)).expect("committed CHM fixture"); + to_document(&bytes, None).expect("convert regression CHM") +} + +fn body_inlines(doc: &Document) -> impl Iterator { + doc.blocks + .iter() + .filter_map(|block| match block { + Block::Heading { content, .. } | Block::Paragraph(content) => Some(content), + _ => None, + }) + .flatten() +} + +fn link<'a>(doc: &'a Document, label: &str) -> &'a LinkTarget { + body_inlines(doc) + .find_map(|inline| match inline { + Inline::Link { content, target } if inlines_to_plain_text(content) == label => { + Some(target) + } + _ => None, + }) + .unwrap_or_else(|| panic!("missing link {label:?}")) +} + +#[test] +fn ascii_topics_use_the_book_encoding_for_percent_encoded_links() { + let doc = regression_document("handmade-legacy-links.chm"); + assert_eq!(link(&doc, "Legacy link"), &LinkTarget::Anchor("中文.htm#参数".into())); +} + +#[test] +fn ascii_toc_uses_the_book_encoding_and_preserves_topic_order() { + let md = to_markdown(fixture().with_file_name("handmade-legacy-links.chm")).unwrap(); + assert!(md.find("# First topic").unwrap() < md.find("# Second topic").unwrap(), "{md}"); + assert!(!md.contains("%D6%D0%CE%C4"), "TOC links must resolve to converted topics: {md}"); +} + +#[test] +fn stylesheet_urls_use_the_page_encoding() { + let md = to_markdown(fixture().with_file_name("handmade-legacy-css.chm")).unwrap(); + assert!(md.contains("VISIBLE-PATH"), "{md}"); + assert!(!md.contains("HIDDEN-PATH"), "page-encoded CSS path must resolve: {md}"); +} + +#[test] +fn stylesheet_charset_is_independent_of_the_book_encoding() { + let md = to_markdown(fixture().with_file_name("handmade-legacy-css.chm")).unwrap(); + assert!(md.contains("VISIBLE-CHARSET"), "{md}"); + assert!(!md.contains("HIDDEN-CHARSET"), "CSS @charset must decode the class selector: {md}"); +} + +#[test] +fn polish_lcid_selects_windows_1250_without_html_metadata() { + let md = to_markdown(fixture().with_file_name("handmade-polish.chm")).unwrap(); + assert!(md.contains("Składnia"), "{md}"); + assert!(md.contains("Zażółć gęślą jaźń."), "{md}"); + assert!(!md.contains('\u{fffd}'), "{md}"); +} + +#[test] +fn topic_title_survives_when_the_body_starts_at_a_subheading() { + let md = to_markdown(fixture().with_file_name("handmade-polish.chm")).unwrap(); + assert!(md.contains("# Command manual\n"), "{md}"); +} + +#[test] +fn local_html_bases_resolve_links_images_and_stylesheets() { + let doc = regression_document("handmade-base.chm"); + let md = to_markdown(fixture().with_file_name("handmade-base.chm")).unwrap(); + for kind in ["relative", "root", "same-book"] { + assert_eq!( + link(&doc, &format!("Link {kind}")), + &LinkTarget::Anchor("shared/target.htm#dest".into()), + "{kind} base must resolve the topic link" + ); + assert!(body_inlines(&doc).any(|inline| matches!( + inline, + Inline::Image { alt, source: ImageSource::Asset(_) } if alt == &format!("Image {kind}") + )), "{kind} base must resolve the embedded image"); + assert!(!md.contains(&format!("HIDDEN-{kind}")), "{kind} base must resolve CSS: {md}"); + } + assert_eq!(doc.assets.len(), 1, "three references share one resolved image"); + assert_eq!(doc.assets[0].origin_part, "shared/dot.png"); +} + +#[test] +fn external_html_base_resolves_urls_and_fragment_references() { + let doc = regression_document("handmade-base.chm"); + assert_eq!( + link(&doc, "Link external"), + &LinkTarget::External("https://example.invalid/shared/target.htm#dest".into()) + ); + assert_eq!( + link(&doc, "Fragment external"), + &LinkTarget::External("https://example.invalid/shared/#self".into()), + "fragment-only references are relative to base href too" + ); + assert!( + body_inlines(&doc).any(|inline| matches!( + inline, + Inline::Image { alt, source: ImageSource::External(url) } + if alt == "Image external" && url == "https://example.invalid/shared/dot.png" + )), + "external base must resolve an image URL without creating an embedded asset" + ); +} + +#[test] +fn html_base_does_not_change_the_scope_of_anchor_definitions() { + let doc = regression_document("handmade-base.chm"); + for kind in ["relative", "root", "same-book", "external"] { + let expected = format!("topics/{kind}.htm#self"); + assert!( + doc.blocks.iter().any(|block| matches!( + block, + Block::Heading { anchor: Some(anchor), .. } if anchor == &expected + )), + "base href must not move the definition {expected}" + ); + } +} + +/// Run with ANYDOC_CHM_SAMPLE pointing to the local ZXR10 command manual: +/// `cargo test --test chm local_chm_manual -- --ignored --nocapture` +#[test] +#[ignore = "requires ANYDOC_CHM_SAMPLE; the external manual is not redistributed"] +fn local_chm_manual() { + let path = std::env::var_os("ANYDOC_CHM_SAMPLE").expect("set ANYDOC_CHM_SAMPLE"); + let bytes = std::fs::read(&path).expect("read local CHM manual"); + assert_eq!(Format::from_bytes(&bytes), Some(format())); + let start = std::time::Instant::now(); + let doc = to_document(&bytes, None).expect("parse the real manual into the shared model"); + let topics: std::collections::HashSet<_> = doc + .blocks + .iter() + .filter_map(|block| match block { + anydoc::model::Block::Paragraph(inlines) => match inlines.as_slice() { + [anydoc::model::Inline::Anchor(path)] if path.ends_with(".html") => Some(path), + _ => None, + }, + _ => None, + }) + .collect(); + assert_eq!(topics.len(), 15_527, "every HTML topic in this manual must survive"); + assert!(!doc.assets.is_empty(), "the manual's embedded images must survive"); + eprintln!("retained {} topics and {} embedded assets", topics.len(), doc.assets.len()); + drop(doc); + let md = to_markdown_bytes(&bytes, None).expect("convert the real compressed CHM manual"); + assert!(md.len() > 1_000_000, "a large manual must not be silently shortened"); + assert!(md.contains("ZXR10"), "product name survives decoding"); + assert!(md.contains("命令"), "Chinese text survives decoding"); + assert!(!md.contains('\u{fffd}'), "declared Chinese encodings must decode without replacement"); + eprintln!( + "converted {} input bytes to {} Markdown bytes in {:?}", + bytes.len(), + md.len(), + start.elapsed() + ); +} diff --git a/tests/fixture-src/chm.py b/tests/fixture-src/chm.py new file mode 100644 index 00000000..f1760df6 --- /dev/null +++ b/tests/fixture-src/chm.py @@ -0,0 +1,261 @@ +"""Build small original CHM fixtures; no vendor manual content is included. + +Run from any directory: python3 tests/fixture-src/chm.py +""" + +from pathlib import Path +import struct +from urllib.parse import quote_from_bytes + + +def encint(value): + groups = [value & 0x7f] + while value >> 7: + value >>= 7 + groups.append((value & 0x7f) | 0x80) + return bytes(reversed(groups)) + + +def archive(parts, compressed=False): + """ITSF v3 with a single PMGL directory and optional LZX content.""" + data = bytearray() + entries = [] + for name, content in sorted(parts.items(), key=lambda item: item[0].lower()): + entries.append(("/" + name.lstrip("/"), int(compressed), len(data), len(content))) + data += content + if compressed: + assert len(data) <= 32768 + # A valid LZX uncompressed block inside the compressed data space. + # Exercise CHM framing/reset metadata without an external CHM compiler. + bits = (3 << 28) | (len(data) << 4) + stream = struct.pack("> 16, bits & 0xffff, 1, 1, 1) + data + if len(data) % 2: + stream += b"\0" + control = struct.pack("> shift) & 1 for shift in range(count - 1, -1, -1)) + + def finish(self): + bits = self.bits + [0] * (-len(self.bits) % 16) + data = bytearray() + for start in range(0, len(bits), 16): + word = 0 + for bit in bits[start:start + 16]: + word = (word << 1) | bit + data += struct.pack("
  • ' + f'' + '
  • ' + '' + ).encode("ascii"), + "a-intro.htm": ( + f'

    Second topic

    Legacy link' + ).encode("ascii"), + "中文.htm": '

    First topic

    参数

    '.encode("gbk"), + })) + css_path = quote_from_bytes("隱藏.css".encode("big5")) + (dest / "handmade-legacy-css.chm").write_bytes(archive({ + "#SYSTEM": system(), + "path.htm": ( + f'' + '

    Page encoded CSS path

    HIDDEN-PATH

    VISIBLE-PATH

    ' + ).encode("big5"), + "declared.htm": ( + '' + '

    CSS declared encoding

    HIDDEN-CHARSET

    VISIBLE-CHARSET

    ' + ).encode("big5"), + "styles/隱藏.css": '.隱藏 { display: none; }'.encode("big5"), + "styles/declared.css": '@charset "big5"; .隱藏 { display: none; }'.encode("big5"), + })) + (dest / "handmade-polish.chm").write_bytes(archive({ + "#SYSTEM": system(lcid=0x0415), + "topic.htm": ( + 'Command manual

    Składnia

    Zażółć gęślą jaźń.

    ' + ).encode("cp1250"), + })) + parts = { + "#SYSTEM": system(compiled="handmade-base.chm"), + "shared/target.htm": b'

    Base target

    ', + "shared/dot.png": png, + "shared/hidden.css": b'.hidden { display: none; }', + } + for kind, base in [ + ("relative", "../shared/"), + ("root", "/shared/"), + ("same-book", "ms-its:handmade-base.chm::/shared/"), + ("external", "https://example.invalid/shared/"), + ]: + parts[f"topics/{kind}.htm"] = ( + f'' + f'

    Base {kind}

    ' + f'

    Link {kind}' + f'Image {kind}

    ' + f'' + f'

    Fragment {kind}

    ' + ).encode("ascii") + (dest / "handmade-base.chm").write_bytes(archive(parts)) + + +def main(): + root = Path(__file__).resolve().parents[1] + dest = root / "fixtures" / "chm" + dest.mkdir(parents=True, exist_ok=True) + system = struct.pack("
      +
    • +
      +
    • +
    """ + intro = """使用指南 +

    使用指南

    中文说明:配置路由器 +

    命令语法
    拓扑图 +

    • 第一步
    • 第二步
    """ + commands = """命令参考 +

    命令参考

    語法

    show version\nshow ip
    +
    命令说明
    show version显示版本
    +

    返回指南拓扑图""" + # Big5 does not encode all simplified characters: use GBK for the shared + # book, and keep a separate Big5 fixture for explicit traditional text. + commands = commands.replace('charset="big5"', 'charset="gbk"') + parts = { + "#SYSTEM": system, + "contents.hhc": b'' + toc.encode("gbk"), + "z-intro.htm": intro.encode("gbk"), + "a-commands.htm": commands.encode("gbk"), + "extra.htm": '

    补充说明

    未列出的主题'.encode(), + "images/dot.png": (root / "fixture-src" / "dot.png").read_bytes(), + } + (dest / "handmade-book.chm").write_bytes(archive(parts)) + (dest / "handmade-compressed.chm").write_bytes(archive(parts, compressed=True)) + (dest / "handmade-big5.chm").write_bytes(archive({ + "topic.htm": ('

    繁體中文

    路由器設定與命令參考' + '

    參數連結

    參數

    ').encode("big5"), + })) + (dest / "handmade-no-toc.chm").write_bytes(archive({ + "b.htm": b"

    Second

    second topic", + "a.htm": b"

    First

    first topic", + })) + regressions(dest, (root / "fixture-src" / "dot.png").read_bytes()) + (dest / "handmade-huffman-spaninfo.chm").write_bytes(huffman_spaninfo()) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/chm/handmade-base.chm b/tests/fixtures/chm/handmade-base.chm new file mode 100644 index 00000000..f945837f Binary files /dev/null and b/tests/fixtures/chm/handmade-base.chm differ diff --git a/tests/fixtures/chm/handmade-big5.chm b/tests/fixtures/chm/handmade-big5.chm new file mode 100644 index 00000000..bef6d111 Binary files /dev/null and b/tests/fixtures/chm/handmade-big5.chm differ diff --git a/tests/fixtures/chm/handmade-book.chm b/tests/fixtures/chm/handmade-book.chm new file mode 100644 index 00000000..73816c0d Binary files /dev/null and b/tests/fixtures/chm/handmade-book.chm differ diff --git a/tests/fixtures/chm/handmade-compressed.chm b/tests/fixtures/chm/handmade-compressed.chm new file mode 100644 index 00000000..5ab9cb59 Binary files /dev/null and b/tests/fixtures/chm/handmade-compressed.chm differ diff --git a/tests/fixtures/chm/handmade-huffman-spaninfo.chm b/tests/fixtures/chm/handmade-huffman-spaninfo.chm new file mode 100644 index 00000000..de11387d Binary files /dev/null and b/tests/fixtures/chm/handmade-huffman-spaninfo.chm differ diff --git a/tests/fixtures/chm/handmade-legacy-css.chm b/tests/fixtures/chm/handmade-legacy-css.chm new file mode 100644 index 00000000..d98ae0fe Binary files /dev/null and b/tests/fixtures/chm/handmade-legacy-css.chm differ diff --git a/tests/fixtures/chm/handmade-legacy-links.chm b/tests/fixtures/chm/handmade-legacy-links.chm new file mode 100644 index 00000000..2339068b Binary files /dev/null and b/tests/fixtures/chm/handmade-legacy-links.chm differ diff --git a/tests/fixtures/chm/handmade-no-toc.chm b/tests/fixtures/chm/handmade-no-toc.chm new file mode 100644 index 00000000..095698d0 Binary files /dev/null and b/tests/fixtures/chm/handmade-no-toc.chm differ diff --git a/tests/fixtures/chm/handmade-polish.chm b/tests/fixtures/chm/handmade-polish.chm new file mode 100644 index 00000000..cd783e36 Binary files /dev/null and b/tests/fixtures/chm/handmade-polish.chm differ diff --git a/tests/snapshots.rs b/tests/snapshots.rs index 25f997c2..1c5ca39d 100644 --- a/tests/snapshots.rs +++ b/tests/snapshots.rs @@ -82,6 +82,7 @@ const STRICT_ANNOTATIONS: bool = true; fn fixtures_detect_from_content() { use anydoc::Format; let expected = [ + ("chm", Some(Format::Chm)), ("doc", Some(Format::Doc)), ("docx", Some(Format::Docx)), ("epub", Some(Format::Epub)), diff --git a/tests/snapshots/snapshots__chm__handmade-base.chm.snap b/tests/snapshots/snapshots__chm__handmade-base.chm.snap new file mode 100644 index 00000000..87d5af6a --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-base.chm.snap @@ -0,0 +1,31 @@ +--- +source: tests/snapshots.rs +expression: output +--- +# Base target + +# Base external + +[Link external](https://example.invalid/shared/target.htm#dest)![Image external](https://example.invalid/shared/dot.png) + +HIDDEN-external + +[Fragment external](https://example.invalid/shared/#self) + +# Base relative + +[Link relative](#base-target)Image relative + +[Fragment relative](#self) + +# Base root + +[Link root](#base-target)Image root + +[Fragment root](#self) + +# Base same-book + +[Link same-book](#base-target)Image same-book + +[Fragment same-book](#self) diff --git a/tests/snapshots/snapshots__chm__handmade-big5.chm.snap b/tests/snapshots/snapshots__chm__handmade-big5.chm.snap new file mode 100644 index 00000000..7ba32de9 --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-big5.chm.snap @@ -0,0 +1,11 @@ +--- +source: tests/snapshots.rs +expression: output +--- +# 繁體中文 + +路由器設定與命令參考 + +[參數連結](#參數) + +## 參數 diff --git a/tests/snapshots/snapshots__chm__handmade-book.chm.snap b/tests/snapshots/snapshots__chm__handmade-book.chm.snap new file mode 100644 index 00000000..bf0d93e7 --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-book.chm.snap @@ -0,0 +1,42 @@ +--- +source: tests/snapshots.rs +expression: output +--- +- [使用指南](#z-intro-htm) + + - [命令参考](#a-commands-htm) + +- [再次阅读](#使用指南) + + + +# 使用指南 + +中文说明:配置路由器 + +[命令语法](#語法)\ +拓扑图 + +- 第一步 +- 第二步 + + + +# 命令参考 + +## 語法 + +``` +show version +show ip +``` + +| 命令 | 说明 | +| --- | --- | +| show version | 显示版本 | + +[返回指南](#使用指南)拓扑图 + +# 补充说明 + +未列出的主题 diff --git a/tests/snapshots/snapshots__chm__handmade-compressed.chm.snap b/tests/snapshots/snapshots__chm__handmade-compressed.chm.snap new file mode 100644 index 00000000..bf0d93e7 --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-compressed.chm.snap @@ -0,0 +1,42 @@ +--- +source: tests/snapshots.rs +expression: output +--- +- [使用指南](#z-intro-htm) + + - [命令参考](#a-commands-htm) + +- [再次阅读](#使用指南) + + + +# 使用指南 + +中文说明:配置路由器 + +[命令语法](#語法)\ +拓扑图 + +- 第一步 +- 第二步 + + + +# 命令参考 + +## 語法 + +``` +show version +show ip +``` + +| 命令 | 说明 | +| --- | --- | +| show version | 显示版本 | + +[返回指南](#使用指南)拓扑图 + +# 补充说明 + +未列出的主题 diff --git a/tests/snapshots/snapshots__chm__handmade-huffman-spaninfo.chm.snap b/tests/snapshots/snapshots__chm__handmade-huffman-spaninfo.chm.snap new file mode 100644 index 00000000..e1dd5089 --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-huffman-spaninfo.chm.snap @@ -0,0 +1,5 @@ +--- +source: tests/snapshots.rs +expression: output +--- +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB diff --git a/tests/snapshots/snapshots__chm__handmade-legacy-css.chm.snap b/tests/snapshots/snapshots__chm__handmade-legacy-css.chm.snap new file mode 100644 index 00000000..be8d018f --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-legacy-css.chm.snap @@ -0,0 +1,11 @@ +--- +source: tests/snapshots.rs +expression: output +--- +# CSS declared encoding + +VISIBLE-CHARSET + +# Page encoded CSS path + +VISIBLE-PATH diff --git a/tests/snapshots/snapshots__chm__handmade-legacy-links.chm.snap b/tests/snapshots/snapshots__chm__handmade-legacy-links.chm.snap new file mode 100644 index 00000000..0243fb5f --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-legacy-links.chm.snap @@ -0,0 +1,18 @@ +--- +source: tests/snapshots.rs +expression: output +--- +- [First topic](#htm) +- [Second topic](#a-intro-htm) + + + +# First topic + +## 参数 + + + +# Second topic + +[Legacy link](#参数) diff --git a/tests/snapshots/snapshots__chm__handmade-no-toc.chm.snap b/tests/snapshots/snapshots__chm__handmade-no-toc.chm.snap new file mode 100644 index 00000000..23a7202f --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-no-toc.chm.snap @@ -0,0 +1,11 @@ +--- +source: tests/snapshots.rs +expression: output +--- +# First + +first topic + +# Second + +second topic diff --git a/tests/snapshots/snapshots__chm__handmade-polish.chm.snap b/tests/snapshots/snapshots__chm__handmade-polish.chm.snap new file mode 100644 index 00000000..d7732492 --- /dev/null +++ b/tests/snapshots/snapshots__chm__handmade-polish.chm.snap @@ -0,0 +1,9 @@ +--- +source: tests/snapshots.rs +expression: output +--- +# Command manual + +## Składnia + +Zażółć gęślą jaźń. diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index cfeb09af..b3cceea5 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -6,7 +6,7 @@ name = "anydoc-wasm" version = "0.2.4" edition = "2024" -description = "WebAssembly bindings for anydoc: convert documents (doc, docx, odt, rtf, epub, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown in the browser" +description = "WebAssembly bindings for anydoc: convert documents (doc, docx, odt, rtf, epub, chm, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown in the browser" license = "MIT" repository = "https://github.com/firecrawl/anydoc" publish = false diff --git a/wasm/README.md b/wasm/README.md index d94bc6c1..f12df4a9 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -4,6 +4,8 @@ WebAssembly bindings for [anydoc](../README.md), plus the source of the demo pag The API mirrors the Rust library, minus the path-based `to_markdown`: wasm has no filesystem, so conversion always starts from bytes. +CHM (`.chm`) files are detected by their ITSF header and parse into the shared document model, including embedded assets. + ```bash npm install @firecrawl/anydoc-wasm ``` diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 6898bb6a..2228a026 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -28,6 +28,7 @@ pub enum Format { Pptx = "pptx", Rtf = "rtf", Epub = "epub", + Chm = "chm", Xlsx = "xlsx", Ods = "ods", Odp = "odp", @@ -45,6 +46,7 @@ impl From for anydoc::Format { Format::Pptx => anydoc::Format::Pptx, Format::Rtf => anydoc::Format::Rtf, Format::Epub => anydoc::Format::Epub, + Format::Chm => anydoc::Format::Chm, Format::Xlsx => anydoc::Format::Excel, Format::Ods => anydoc::Format::Ods, Format::Odp => anydoc::Format::Odp, @@ -65,6 +67,7 @@ impl From for Format { anydoc::Format::Pptx => Format::Pptx, anydoc::Format::Rtf => Format::Rtf, anydoc::Format::Epub => Format::Epub, + anydoc::Format::Chm => Format::Chm, anydoc::Format::Excel => Format::Xlsx, anydoc::Format::Ods => Format::Ods, anydoc::Format::Odp => Format::Odp, @@ -74,9 +77,10 @@ impl From for Format { } /// Detect the format from the content itself: the signature and identity each -/// container specification designates (PDF header, RTF open group, OLE stream -/// names, ZIP package mimetype/content types). Plain-text formats (CSV) carry -/// no signature and return `undefined`; so does anything unrecognized. +/// container specification designates (CHM ITSF header, PDF header, RTF open +/// group, OLE stream names, ZIP package mimetype/content types). Plain-text +/// formats (CSV) carry no signature and return `undefined`; so does anything +/// unrecognized. #[wasm_bindgen(js_name = formatFromBytes)] pub fn format_from_bytes(bytes: &[u8]) -> Option { anydoc::Format::from_bytes(bytes).map(Format::from) diff --git a/wasm/test.mjs b/wasm/test.mjs index 86b675ec..56d44b1a 100644 --- a/wasm/test.mjs +++ b/wasm/test.mjs @@ -24,6 +24,8 @@ const CSV = await readFile(fixture('csv/sheet.csv')) const PDF = await readFile(fixture('pdf/text.pdf')) const ENCRYPTED = await readFile(fixture('malformed/encrypted--errors.odt')) const MIXED = await readFile(fixture('pdf/handmade-mixed.pdf')) +const CHM = await readFile(fixture('chm/handmade-book.chm')) +const CHM_COMPRESSED = await readFile(fixture('chm/handmade-compressed.chm')) test('toMarkdownBytes converts in memory', () => { const markdown = toMarkdownBytes(RICH, 'docx') @@ -59,6 +61,31 @@ test('toDocument carries embedded assets as Uint8Arrays', () => { assert.equal(image.id, document.assets.indexOf(image)) }) +test('chm converts through the bytes and document APIs', () => { + const markdown = toMarkdownBytes(CHM) + assert.equal(toMarkdownBytes(CHM, 'chm'), markdown) + assert.equal(toMarkdownBytes(CHM_COMPRESSED), markdown) + for (const title of ['使用指南', '命令参考', '补充说明']) assert.ok(markdown.includes(title)) + assert.match(markdown, /配置路由器/) + assert.match(markdown, /\| 命令 \| 说明 \|/) + assert.match(markdown, /show version/) + assert.match(markdown, /\[命令语法\]\(#[^)]+\)/) + + const document = toDocument(CHM, 'chm') + assert.deepEqual(toDocument(CHM), document) + assert.equal(document.blocks.filter((block) => block.kind === 'heading' && block.level === 1).length, 3) + assert.ok(document.blocks.some((block) => block.kind === 'table')) + assert.equal(document.assets.length, 1) + assert.equal(document.assets[0].mediaType, 'image/png') + assert.ok(document.assets[0].data instanceof Uint8Array) + assert.ok(document.assets[0].data.length > 0) +}) + +test('chm SpanInfo streams Huffman frames across dictionary resets', async () => { + const bytes = await readFile(fixture('chm/handmade-huffman-spaninfo.chm')) + assert.equal(toMarkdownBytes(bytes).trim(), 'A'.repeat(65536) + 'B'.repeat(65536)) +}) + test('format detection reads content, extension, and path', () => { assert.equal(formatFromBytes(RICH), 'docx') // CSV carries no signature: only the extension names it. @@ -67,6 +94,9 @@ test('format detection reads content, extension, and path', () => { assert.equal(formatFromExtension('xls'), 'xlsx') assert.equal(formatFromPath('/tmp/report.odt'), 'odt') assert.equal(formatFromPath('/tmp/report.unknown'), undefined) + assert.equal(formatFromBytes(CHM), 'chm') + assert.equal(formatFromExtension('.CHM'), 'chm') + assert.equal(formatFromPath('/tmp/help.chm'), 'chm') }) // `code` is what callers branch on, so every kind of failure is pinned here. diff --git a/wasm/www/index.html b/wasm/www/index.html index 2e610db4..a79d1f2e 100644 --- a/wasm/www/index.html +++ b/wasm/www/index.html @@ -6,7 +6,7 @@ anydoc by Firecrawl