diff --git a/README.md b/README.md index 517e2ad..4b07000 100644 --- a/README.md +++ b/README.md @@ -125,12 +125,56 @@ open /Volumes/Cases/ios_reports/index.html | `--no-hash` | off | Skip computing the SHA-256 of each input archive (hashing is on by default). | | `--skip-existing` | off | Skip a zip whose output folder already exists and is non-empty (resume a partial run). | | `--dry-run` | off | Print the exact commands without running the tool. | +| `--coverage` | off | **Developer mode.** Enable the LEAPP App Inventory artifacts on each run and aggregate every report into `batch_apps.sqlite` (see [Coverage mode](#coverage-mode-developers)). | | `-- ` | — | Everything after a literal `--` is appended verbatim to every LEAPP run (e.g. `-- -p fast` for an iLEAPP profile). | > **Passing extra args:** batch-leapp owns `-o` (it gives each extraction its own output folder), so don't pass your own `-o` through `--` — it would send every report to the same place. To rename the report folder use iLEAPP's `--custom_output_folder` instead (e.g. `-- --custom_output_folder MyReports`); it renames the folder *inside* each per-zip output dir, so there's no collision and the index/manifest still find every report. --- +## Coverage mode (developers) + +`--coverage` answers the question *“which installed apps are we not parsing?”* +across a whole batch of test images. It is meant for LEAPP developers, not for +casework. + +```bash +python3 batch_leapp.py ~/test_images ~/reports --leapp ~/iLEAPP/ileapp.py --coverage +``` + +What it does: + +1. **Enables the App Inventory artifacts** (`scripts/alternate_artifacts/appInventory.py` + in the iLEAPP/ALEAPP source checkouts) for every run, via each tool's + `--custom_artifacts_path` option. For ALEAPP checkouts that predate that + option the module is staged into `scripts/artifacts/` for the duration of + the batch and removed afterwards. Normal (non-coverage) runs never see + these artifacts. +2. **Aggregates every report** into `batch_apps.sqlite` at the output root: + per-extraction device identifiers (`extractions`), installed apps + (`installed_apps`), every file mapped to its owning app (`app_files`), and + which files each LEAPP module matched (`artifact_files`). +3. **Computes coverage views**: + - `v_apps_not_parsed` — apps with data on disk that no app-specific module + touched, per extraction + - `v_apps_not_parsed_rollup` — the same across all extractions, ranked + - `v_unknown_containers` — iOS app containers whose owner app is unknown + - `v_module_app_spread` — classifies modules as *generic* when they match + files for 10+ different apps (e.g. `userDefaults`); generic matches don't + make an app count as parsed + +Re-aggregate an existing batch output (no re-run needed) with: + +```bash +python3 batch_coverage.py ~/reports +``` + +Notes: requires LEAPP **source checkouts** that contain the App Inventory +module (compiled binaries don't bundle it); RLEAPP/VLEAPP batches aggregate +run metadata but have no app inventory by design. + +--- + ## Building standalone binaries You can package batch-leapp into native binaries that don't need Python installed — a double-clickable **GUI app** and a single-file **CLI binary** — using [PyInstaller](https://pyinstaller.org/). Build scripts and icons are included. diff --git a/batch_coverage.py b/batch_coverage.py new file mode 100644 index 0000000..413f7ff --- /dev/null +++ b/batch_coverage.py @@ -0,0 +1,494 @@ +#!/usr/bin/env python3 +""" +batch_coverage.py — app parsing-coverage support for batch_leapp. + +Two responsibilities: + +1. enable_coverage(): make a LEAPP run include the developer-only + "App Inventory" artifacts (scripts/alternate_artifacts/appInventory.py in + the iLEAPP/ALEAPP repos), which write extractioninfo / + installedappinventory / appfileinventory tables into each run's + _lava_artifacts.db. + - When the tool supports --custom_artifacts_path (iLEAPP; ALEAPP since + PR #939) the module is enabled with extra arguments. + - Otherwise (older ALEAPP checkouts) the module is staged (copied) into + scripts/artifacts/ for the duration of the batch and removed afterwards. + +2. aggregate(): after a batch (or standalone on any batch output folder), + walk every report's _lava_artifacts.db and merge the inventory tables, + the framework's artifact-match registry tables and the manifest into a + single batch_apps.sqlite with views that answer "which installed apps + were NOT parsed by the tooling?". + +Standalone usage (re-aggregate an existing batch output directory): + + python3 batch_coverage.py /path/to/batch_output +""" + +import argparse +import json +import re +import shutil +import sqlite3 +import sys +from pathlib import Path + +COVERAGE_DB_NAME = "batch_apps.sqlite" +INVENTORY_MODULE = "appInventory" + +# Android package-owned locations (mirrors the ALEAPP inventory artifact). +_PKG = r'([A-Za-z0-9_][A-Za-z0-9_.\-]*)' +_ANDROID_LOCATIONS = tuple(re.compile(p) for p in ( + r'Android/data/' + _PKG + r'(?=/|$)', + r'Android/media/' + _PKG + r'(?=/|$)', + r'Android/obb/' + _PKG + r'(?=/|$)', + r'data/data/' + _PKG + r'(?=/|$)', + r'data/user/\d+/' + _PKG + r'(?=/|$)', + r'data/user_de/\d+/' + _PKG + r'(?=/|$)', + r'data/app/(?:~~[^/]+/)?' + _PKG + r'-[^/]+(?=/|$)', +)) + +# iOS container GUIDs (mirrors the iLEAPP inventory artifact). +_IOS_GUID_RE = re.compile( + r'containers/(?:Bundle/Application|Data/Application|Shared/AppGroup|Data/PluginKitPlugin)' + r'/([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?=/|$)', + re.IGNORECASE) +_ITUNES_DOMAIN_RE = re.compile(r'^AppDomain(?:Group|Plugin)?-([^/]+)/') + + +# -------------------------------------------------------------------------- +# Enabling the inventory artifacts on a LEAPP run +# -------------------------------------------------------------------------- + +def enable_coverage(leapp: Path, tool: str, log=print): + """Prepare a LEAPP source checkout to run the App Inventory artifacts. + + Returns (extra_args, cleanup) where extra_args is a list to append to + every LEAPP command line and cleanup is a callable to run once the whole + batch is finished. Logs a warning and returns ([], noop) when the tool + has no inventory support (RLEAPP/VLEAPP, compiled binaries, or a checkout + that predates the module). + """ + def noop(): + return None + + if leapp.suffix.lower() != ".py": + log("coverage: only LEAPP source checkouts are supported " + "(compiled binaries do not bundle scripts/alternate_artifacts) — " + "inventory artifacts disabled, aggregation will still run.") + return [], noop + + alternate = leapp.resolve().parent / "scripts" / "alternate_artifacts" / f"{INVENTORY_MODULE}.py" + if not alternate.is_file(): + log(f"coverage: {alternate} not found — this {tool} checkout has no " + "App Inventory module; inventory artifacts disabled.") + return [], noop + + if _supports_custom_artifacts_path(leapp): + return ["--custom_artifacts_path", str(alternate.parent)], noop + + if tool == "ALEAPP": + # Fallback for ALEAPP checkouts that predate --custom_artifacts_path + # (abrignoni/ALEAPP#939): stage the module for the batch. + log("coverage: this ALEAPP has no --custom_artifacts_path option, " + "staging the module instead.") + staged = alternate.parent.parent / "artifacts" / alternate.name + if staged.exists(): + log(f"coverage: {staged} already present, leaving it in place.") + return [], noop + shutil.copy2(alternate, staged) + log(f"coverage: staged {alternate.name} into {staged.parent}") + + def cleanup(): + try: + staged.unlink() + log(f"coverage: removed staged {staged}") + except OSError as ex: + log(f"coverage: could not remove staged {staged}: {ex}") + return [], cleanup + + log(f"coverage: {tool} does not support --custom_artifacts_path — " + "inventory artifacts disabled, aggregation will still run.") + return [], noop + + +def _supports_custom_artifacts_path(leapp: Path) -> bool: + """True when the LEAPP script accepts --custom_artifacts_path.""" + try: + return "custom_artifacts_path" in leapp.read_text(encoding="utf-8", + errors="replace") + except OSError: + return False + + +# -------------------------------------------------------------------------- +# Aggregation +# -------------------------------------------------------------------------- + +_SCHEMA = """ +CREATE TABLE extractions ( + extraction_id INTEGER PRIMARY KEY, + output_folder TEXT, -- batch destination folder name + report_folder TEXT, -- LEAPP report folder name + report_path TEXT, -- absolute path of the report folder + zip TEXT, -- input archive (from manifest.json) + sha256 TEXT, + tool TEXT, + tool_version TEXT, + extraction_type TEXT, + input_name TEXT, + input_path TEXT, + os_version TEXT, + device_name TEXT, + model TEXT, + manufacturer TEXT, + serial_number TEXT, + time_zone TEXT, + build_id TEXT, + has_inventory INTEGER -- 1 when the App Inventory artifacts ran +); +CREATE TABLE installed_apps ( + extraction_id INTEGER, + app_id TEXT, + location_type TEXT, + container_path TEXT, + container_uuid TEXT, + install_time INTEGER, + update_time INTEGER, + installer TEXT, + data_source TEXT +); +CREATE TABLE app_files ( + extraction_id INTEGER, + app_id TEXT, + location_type TEXT, + container_uuid TEXT, + file_path TEXT, + file_size TEXT, + modified_time TEXT +); +CREATE TABLE artifact_patterns ( + extraction_id INTEGER, + pattern_id INTEGER, + module_name TEXT, + artifact_name TEXT, + regex TEXT +); +CREATE TABLE artifact_files ( + extraction_id INTEGER, + file_path TEXT, + app_id TEXT, -- resolved by the aggregator + module_name TEXT -- one row per (file, matching module) +); +CREATE INDEX idx_app_files ON app_files(extraction_id, app_id); +CREATE INDEX idx_artifact_files ON artifact_files(extraction_id, app_id); +CREATE INDEX idx_installed ON installed_apps(extraction_id, app_id); + +-- How many different apps each module matched files for, per extraction. +-- Modules that touch many apps (userDefaults, appGrouplisting, packageInfo, +-- the framework's generic plist/db sweeps, ...) are 'generic': they match +-- every app's housekeeping files without decoding app content, so they must +-- not make an app count as parsed. +CREATE VIEW v_module_app_spread AS +SELECT extraction_id, module_name, + COUNT(DISTINCT app_id) AS apps_matched, + COUNT(DISTINCT app_id) >= 10 AS is_generic +FROM artifact_files +WHERE app_id != '' +GROUP BY extraction_id, module_name; + +-- one row per (extraction, app) with disk/matched file counts. +-- files_matched counts any artifact match; files_matched_specific counts +-- only matches by app-specific (non-generic) modules. +CREATE VIEW v_app_coverage AS +SELECT u.extraction_id, u.app_id, + (SELECT COUNT(*) FROM app_files f + WHERE f.extraction_id = u.extraction_id AND f.app_id = u.app_id) AS files_on_disk, + (SELECT COUNT(DISTINCT m.file_path) FROM artifact_files m + WHERE m.extraction_id = u.extraction_id AND m.app_id = u.app_id) AS files_matched, + (SELECT COUNT(DISTINCT m.file_path) FROM artifact_files m + JOIN v_module_app_spread s + ON s.extraction_id = m.extraction_id + AND s.module_name = m.module_name + WHERE m.extraction_id = u.extraction_id AND m.app_id = u.app_id + AND s.is_generic = 0) AS files_matched_specific, + EXISTS (SELECT 1 FROM installed_apps i + WHERE i.extraction_id = u.extraction_id AND i.app_id = u.app_id) AS in_inventory +FROM (SELECT DISTINCT extraction_id, app_id FROM app_files WHERE app_id != '' + UNION + SELECT DISTINCT extraction_id, app_id FROM installed_apps WHERE app_id != '') u; + +-- apps with data on disk that no app-specific artifact touched +CREATE VIEW v_apps_not_parsed AS +SELECT e.tool, e.input_name, e.os_version, c.* +FROM v_app_coverage c +JOIN extractions e ON e.extraction_id = c.extraction_id +WHERE c.files_matched_specific = 0 +ORDER BY e.input_name, c.app_id; + +-- cross-extraction rollup: which apps are consistently missed +CREATE VIEW v_apps_not_parsed_rollup AS +SELECT app_id, + COUNT(*) AS extractions_present, + SUM(files_matched_specific = 0) AS extractions_unparsed, + SUM(files_on_disk) AS total_files_on_disk +FROM v_app_coverage +GROUP BY app_id +HAVING extractions_unparsed > 0 +ORDER BY extractions_unparsed DESC, total_files_on_disk DESC; + +-- iOS app containers whose owner could not be identified (no metadata plist, +-- not in applicationState.db) — likely uninstalled/orphaned apps; the bundle +-- id is unknown so they cannot appear in v_app_coverage. +CREATE VIEW v_unknown_containers AS +SELECT e.tool, e.input_name, f.extraction_id, f.container_uuid, + f.location_type, COUNT(*) AS files_on_disk +FROM app_files f +JOIN extractions e ON e.extraction_id = f.extraction_id +WHERE f.app_id = '' AND f.container_uuid != '' +GROUP BY f.extraction_id, f.container_uuid, f.location_type; +""" + + +def _table_exists(con, name): + return con.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,) + ).fetchone() is not None + + +def _columns(con, table): + return [r[1] for r in con.execute(f"PRAGMA table_info({table})")] + + +def _load_manifest(output_dir: Path): + """Map batch destination folder name -> manifest row (zip, sha256).""" + manifest = output_dir / "manifest.json" + if not manifest.is_file(): + return {} + try: + with open(manifest, encoding="utf-8") as f: + data = json.load(f) + return {row.get("output_folder", ""): row + for row in data.get("extractions", [])} + except (OSError, json.JSONDecodeError): + return {} + + +def _load_lava_json(report_dir: Path): + for candidate in report_dir.glob("*.lava"): + try: + with open(candidate, encoding="utf-8") as f: + return json.load(f) + except (OSError, json.JSONDecodeError): + continue + return {} + + +def _extraction_props(con): + """extractioninfo table -> {property: value}.""" + if not _table_exists(con, "extractioninfo"): + return {} + props = {} + for prop, value in con.execute("SELECT property, value FROM extractioninfo"): + props.setdefault(prop, value) + return props + + +def _first(props, *keys): + for key in keys: + if props.get(key): + return props[key] + return "" + + +def _norm_path(path, input_path): + """Normalize a recorded path so app_files and artifact_files line up. + + Directory-seeker source paths are absolute under the extraction root while + the inventory strips that root; strip it here too. + """ + path = str(path).replace("\\", "/") + if input_path: + root = str(input_path).replace("\\", "/").rstrip("/") + if root and path.startswith(root): + path = path[len(root):] + return path.lstrip("/") + + +def _map_path_to_app(path, tool, uuid_map): + """Best-effort app id for an extraction file path.""" + if tool == "ALEAPP": + for pattern in _ANDROID_LOCATIONS: + match = pattern.search(path) + if match: + return match.group(1) + return "" + match = _IOS_GUID_RE.search(path) + if match: + return uuid_map.get(match.group(1).upper(), "") + match = _ITUNES_DOMAIN_RE.match(path) + if match: + return match.group(1) + return "" + + +def _ingest_report(out_con, extraction_id, report_dir, dest_name, manifest_row, log): + lava_db = report_dir / "_lava_artifacts.db" + con = sqlite3.connect(f"file:{lava_db}?mode=ro", uri=True) + try: + props = _extraction_props(con) + lava_json = _load_lava_json(report_dir) + parser_info = lava_json.get("parser_info", {}) + + tool = _first(props, "LEAPP Name") or parser_info.get("leapp_name", "") + input_path = _first(props, "Input Path") or lava_json.get("param_input", "") + has_inventory = int(_table_exists(con, "appfileinventory") + or _table_exists(con, "installedappinventory")) + + out_con.execute( + "INSERT INTO extractions VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + (extraction_id, dest_name, report_dir.name, str(report_dir), + manifest_row.get("zip", ""), manifest_row.get("sha256", ""), + tool, + _first(props, "LEAPP Version") or parser_info.get("leapp_version", ""), + _first(props, "Extraction Type") or lava_json.get("param_type", ""), + _first(props, "Input Name") or Path(str(input_path)).name, + str(input_path), + _first(props, "iOS Version", "Android Version"), + _first(props, "Device Name"), + _first(props, "Model", "Product Type"), + _first(props, "Manufacturer"), + _first(props, "Serial Number"), + _first(props, "Time Zone"), + _first(props, "Build ID", "ProductBuildVersion"), + has_inventory)) + + # installed_apps (iOS and Android tables have different shapes) + uuid_map = {} + if _table_exists(con, "installedappinventory"): + cols = _columns(con, "installedappinventory") + if "bundle_id" in cols: # iLEAPP shape + for bid, ctype, cpath, cuuid, src in con.execute( + "SELECT bundle_id, container_type, container_path," + " container_uuid, data_source FROM installedappinventory"): + out_con.execute( + "INSERT INTO installed_apps VALUES (?,?,?,?,?,?,?,?,?)", + (extraction_id, bid, ctype, cpath, cuuid, None, None, "", src)) + if cuuid and bid: + uuid_map[cuuid.upper()] = bid + else: # ALEAPP shape + for pkg, it, ut, installer, cpath, src in con.execute( + "SELECT package_name, install_time, update_time," + " installer, code_path, data_source FROM installedappinventory"): + out_con.execute( + "INSERT INTO installed_apps VALUES (?,?,?,?,?,?,?,?,?)", + (extraction_id, pkg, "apk", cpath, "", it, ut, installer, src)) + + # app_files + if _table_exists(con, "appfileinventory"): + cols = _columns(con, "appfileinventory") + id_col = "bundle_id" if "bundle_id" in cols else "package_name" + type_col = "container_type" if "container_type" in cols else "location_type" + uuid_expr = "container_uuid" if "container_uuid" in cols else "''" + for app_id, ltype, cuuid, fpath, fsize, mtime in con.execute( + f"SELECT {id_col}, {type_col}, {uuid_expr}, file_path," + " file_size, modified_time FROM appfileinventory"): + out_con.execute( + "INSERT INTO app_files VALUES (?,?,?,?,?,?,?)", + (extraction_id, app_id, ltype, cuuid, + _norm_path(fpath, input_path), fsize, mtime)) + if cuuid and app_id: + uuid_map.setdefault(cuuid.upper(), app_id) + + # framework registry: patterns and matched files (exclude the + # inventory module itself, or every app would look "parsed") + if _table_exists(con, "_artifact_search_patterns"): + for pid, module, artifact, regex in con.execute( + "SELECT id, module_name, artifact_name, regex" + " FROM _artifact_search_patterns"): + out_con.execute( + "INSERT INTO artifact_patterns VALUES (?,?,?,?,?)", + (extraction_id, pid, module, artifact, regex)) + if _table_exists(con, "_artifact_pattern_to_file"): + seen = set() + for fpath, module in con.execute(""" + SELECT DISTINCT f.file_path, p.module_name + FROM _artifact_pattern_to_file link + JOIN _file_path_list f ON f.id = link.file_path_id + JOIN _artifact_search_patterns p + ON p.id = link.artifact_search_pattern_id + WHERE p.module_name != ?""", (INVENTORY_MODULE,)): + norm = _norm_path(fpath, input_path) + if (norm, module) in seen: + continue + seen.add((norm, module)) + out_con.execute( + "INSERT INTO artifact_files VALUES (?,?,?,?)", + (extraction_id, norm, + _map_path_to_app(norm, tool, uuid_map), module)) + finally: + con.close() + return True + + +def aggregate(output_dir, log=print): + """Merge every report under output_dir into batch_apps.sqlite. + + Full rebuild each time (idempotent). Returns the path of the coverage DB, + or None when no reports were found. + """ + output_dir = Path(output_dir).expanduser().resolve() + lava_dbs = sorted(p for p in output_dir.rglob("_lava_artifacts.db")) + if not lava_dbs: + log(f"coverage: no _lava_artifacts.db found under {output_dir}") + return None + + db_path = output_dir / COVERAGE_DB_NAME + if db_path.exists(): + db_path.unlink() + out_con = sqlite3.connect(db_path) + out_con.executescript(_SCHEMA) + + manifest = _load_manifest(output_dir) + count = 0 + for extraction_id, lava_db in enumerate(lava_dbs, start=1): + report_dir = lava_db.parent + rel = report_dir.relative_to(output_dir) + dest_name = rel.parts[0] if rel.parts else report_dir.name + try: + _ingest_report(out_con, extraction_id, report_dir, dest_name, + manifest.get(dest_name, {}), log) + count += 1 + except sqlite3.Error as ex: + log(f"coverage: skipped {report_dir} ({ex})") + out_con.commit() + + apps = out_con.execute("SELECT COUNT(DISTINCT app_id) FROM v_app_coverage").fetchone()[0] + unparsed = out_con.execute("SELECT COUNT(*) FROM v_apps_not_parsed").fetchone()[0] + with_inventory = out_con.execute( + "SELECT COUNT(*) FROM extractions WHERE has_inventory = 1").fetchone()[0] + out_con.close() + + log(f"coverage: aggregated {count} extraction(s) " + f"({with_inventory} with inventory data) into {db_path}") + log(f"coverage: {apps} distinct app(s); {unparsed} app/extraction " + "pair(s) with no artifact coverage (see v_apps_not_parsed / " + "v_apps_not_parsed_rollup)") + return db_path + + +def main(): + parser = argparse.ArgumentParser( + description="Aggregate LEAPP batch reports into batch_apps.sqlite " + "for app parsing-coverage analysis.") + parser.add_argument("output_dir", type=Path, + help="Batch output directory containing the report folders") + args = parser.parse_args() + if not args.output_dir.is_dir(): + print(f"Error: not a directory: {args.output_dir}", file=sys.stderr) + return 2 + return 0 if aggregate(args.output_dir) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/batch_leapp.py b/batch_leapp.py index 26c53af..d37b179 100644 --- a/batch_leapp.py +++ b/batch_leapp.py @@ -32,6 +32,8 @@ from pathlib import Path from urllib.parse import quote +import batch_coverage + # Embedded leapps.org brand assets (downscaled) so the generated index.html # is fully self-contained and needs no external files. LEAPP_LOGO_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAADkCAYAAAAisRkYAAAAAXNSR0IArs4c6QAAAJxlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAqY3AAAJbAACpjcAAAlsAAWQAAAHAAAABDAyMTCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAASygAwAEAAAAAQAAAOQAAAAAfFDk9gAAAAlwSFlzAAALEgAACxIB0t1+/AAAA01pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+NzE5ODMvMTAwMDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzE5ODMvMTAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjEyNTA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjY1NTM1PC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOkV4aWZWZXJzaW9uPjAyMTA8L2V4aWY6RXhpZlZlcnNpb24+CiAgICAgICAgIDxleGlmOkZsYXNoUGl4VmVyc2lvbj4wMTAwPC9leGlmOkZsYXNoUGl4VmVyc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjk1MDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgqtBZBGAABAAElEQVR4AeydB4BVxdXHD2wvsLBLr4s0ARFQsaCixl6ixpKixpbEaIxRk1g+NQmWRGOaRqPRGEsSW6Im9q5YUFGaIL0vHbYv28v7zm9273L38cq9b19buAfuvvdumTtzZuY/Z86cc0bEI48DHgc8Dngc8DjgccDjgMcBjwMeBzwOeBzwOOBxwOOAxwGPAx4HPA54HPA44HHA44DHAY8DHgf2Lg50i2Jxe2haffUY0PZZoJ+99MjVI1uPTD3S9EjVI0WP7m2HlQfrU0975HHA40CSccDXlh8+W9qOZv1s0qNBj3o9avSo0qNCjxI9tuuxre1zp352mjoDEln69ql6fE2PQ/QYrUc/PQAujzwOeBzwOAAHADCAa4Uen+nxvh5f6FGnh2uKBLCQnC5qOybqZyRpuM6o94DHAY8DewQHkNC+1OOJtqPMTancgs35mvgv9Rjj5iXevR4HPA54HAjAgWV67lY9nglwLeApp4DVR5/+ox7fDZiKd9LjgMcBjwORc+AxffTnepSGS8IJYO2riTypxwHhEvOuexzwOOBxIEIOoNdiBrcy1PPhAGt/ffi/euwTKhHvmscBjwMeB6LAAcDqTD2WBEsrFGABUm/oweqfRx4HPA54HIgHB9BrnaTH+kAvCwZY2E69rscRgR5ycy49PV1ycnLMkZ2dLRkZGcK51NRUSUlJkW7durUfbtL17vU44HEgfhzw+XxiHc3NzdLU1CQNDQ1SX18vNTU1Ul1dbQ7ORYEwfThND+y6OlAwwPq93vWzDnc6/DF06FCZNm2aHHLIITJu3DgZMmSI9O7d2wCWHai6d+9ugMphst5tHgc8DiQJBwCulpYWsYAL0AKwysrKZOPGjbJ06VL57LPP5NNPPzW/I8z2nfrcTf7PBgKsI/Wmd/RI97851O+pU6fKVVddJaeccooUFGCq5ZHHAY8DezMHiouL5dVXX5X77rtP5s6d65YVGJZilP6p/UF/wMJt5i09jrHfFOp7z5495Re/+IVceeWVkpWF8btHHgc8Dngc2MUBpC9A69e//rXs3OnKQwcsOkUPXIAM+QPW8Xr2TT38z7fe7fd39OjR8thjj8nhhx/ud8X76XHA44DHgY4c+PDDD+WSSy6RNWvWdLwQ/Bc+i8fqMdO6pbv1pe3zQv10BFYTJkyQ1157zQMrPwZ6Pz0OeBwIzIHp06cbzNh3X0w7HRH4BCa1kx2c8vXsQj0Gt18N8gXF+ltvvSUuXhwkJe+0xwGPA3sbBxYvXiwnnHCCbN682UnRi/Qm7EGJAGFCvPAJTdJjkPkW4g8rfQ899JAHViF45F3yOOBxIDgHmJ09+OCDxrQp+F3tV4bot/2sX/YpIa43donLuqfD52WXXSYnn3xyh3PeD48DHgc8DrjhwOmnny6XXnqpk0fAqHa3QDtAPaoXLgmVwoABA8zy5KBBYQWxUMl41zwOeBxwyIH169fLP//5T2Ea1b9/fznvvPPk4IMPdvh0ct9WVFQkBx10kOzYsSNcRh/SGy7nJswYLCq0vgT7/Pa3vy0eWAXjjnfe40B0OTB//nz5xje+IYCWRY888og8/fTT8vWvf9061WU/hw0bJueee6488MAD4cowwrrBmhJm6In+1slAn7jRnH322YEueec8DngciAEHbr/99g5gxSuwacLusa4Ou8quT+ecc47g9RKGBuj1NO6x7szR78RfD0q42Oy/P8p6jzwOeByINQdwf9mwYUPA1yxatEg+//zzgNe62snJkyfLwIEDw2UbbMrmJjtgmRPBnsSEAat2jzwOeByIPQcICoDOKhDhx/fiiy8GutTlzuFnPHbs2HD5RqAy+GTpsPjBtDAoYdXukccBOwdaVAqorm6QyqpaKauoldKKGikv10N/V1XVy86aeqmtbZC6evXsb8TDv8U4zVpp0ClTU7pLalp3SU9LlYz0VMnKTJPsbI3woUePnAzpkZupR4b01M/cHI50yc5Kl8yM1D3eef7EE080vngWv+yf+OjddtttJqiA/XxX/D5q1Ch57733QmUdbOoAWJl6wgKvgA+iIPNo7+VAeWWtbNlaIes2lsqaohJZr59btlVKSWm1VO6sk9q6RmlUUGrW0Z/pjLDVQNsatAYQav8ekINtt5uH7I8qoHXv3k1S9EhNTWkHNECrZ48sye+VLX3yc6Rfnx7Sv28P6VfQQ/oU5EjvvGy9nmnuD/i+LnLy2GOPNf65tbW1u+V4xYoVJiIC93R1coAt6K/AqHaQAsFCAla/fv2436O9gAMtLT7ZvK1Clq3aJouWbpYlK7dJkQJUSVm1KnsbVUryGekGMOHoxqH/UlNVWmrXMkSXUbyztq5BalRi21G6U3z6GwmvNUaT6jaQ1vT9GSp55WZnSK+8LOlbkCuD+ufJ4IG9ZIgegwbkKajl6rVsI6FFN4fRT23MmDGCkeWcOXOkR48e0qtXL5Vgy1V6rTLlfu6552RPACwH2AI2mRmgBVKEkmkbDwMzPi8vL/AF7+wewQGmdIuXb5HP56+X+V9tlLUqRVWoVNXc3CLdddqGlAM4peu0LRGkeKSksKifGkmtdSveABlByivVaWmxSn4rVm83oIa0Z+U9V6eZBb1zZEC/njJ0UC8pHJovw4fkGzDrm5+rU86QmpEAb4zdKYJcfv/735czzjhDLrjgAsEOsrS01ADY3/72N+OXV1JS0uXDOQHEYQhdu1kltFqf+RHqodxcgpB6tCdxYOuOSpmzoEg+mr1aFizeJFu3V5ppXQoApUdaWoo5ulKZWyPYtgIU0GYnZqqVVXVSruC8ck0bmOkNTDdzVC/GFHOgSmEA2MjhBTJiWIGRzPookKFbSwT98Ic/NK/dvn27WRkk0ieS1wsvvCCvvPKKiXzQ1ePPEZE4DDFcdQAsto4PSYQ29qjrcwDJ47O5a+W9WStl3qINsqNE4xNpR2Y6ZQFV1y9l4BIgnQFoSFv+YFZX3ygbt5TL+k1l8skXa0wCaQpkSFxMLYcN7i37DO8jowr76meB7Du6v5mGBn5T9M4CVCjX//3vf7dbhKelpckBBxwgM2bMEAJndnXKzDTqqXDFMBhlSVgdhyK/R6lkmORR1+QA06S5Ck6vv7dEPvl8rWzZXmF04nRIVuY8agWylBRV8JtusWv8rladWZVOj1eu3SHvfLjc6OvSFNx/fsWxcsHZsQUL7LDOPPNMmTdvXocqamxslNmzZxtr93vuuccEz+xwQxf74RBbDEZZrTUsYDGf9qhrcQBdzlszl8pLb38lS5ZvVdOCJjPFS5QeqmtxrzW3KPO7K5BhfmERer0Hn/hYDpo0TPYdFdhWyro30k/ipV999dW7gZU9PaaHP/vZz2TKlClmHwX7ta703SG2dAAsZOSg1CpG76qwoDd6F5KCAxs2l8kLry2U195dLBs3l5spEHqazAxPSo5GBTF1xszjrvvfkb/+9psx4SubODgxDmUDiD/+8Y9dGrCsDWmMOUzwCjIYZYlNHmAFZ1SXubJuQ4k887958ppO/dBNpavSnGV+j6LPAabSn89fJ48/O1suv7DTu+HtlsH333+/g5HtbjfYTjA9xNzBwWqb7ank+RoJYCVP7r2cuObAJjXofOqFOfLSW4uMISdTvkStarnOfBd+AOv8R5/5TA45oFCm7EecuegR5gpOCcNSjq4KWMzgnJKj4ZcE3STq9OXefZ3jANOSZ/83V555ab5sU5MEgCozzsvvxqhd/2DYaQw59ZN/Rqtv2mGb7RSN0vzv2DiN4aeywTIAtazdDWdMu2tb2bN9N0nxJ8HEamNNbaP8VqeGf/v9d4wLkX+Wtm3bJn/605/kq6++ElxQTj31VDnssMMknJmQA2PK9lexQTHH3kCOAGtvYERXKiNK39feXSJ/e/ITWbVuh1GkxwOoAKSWZp8xJgVgMCjFp6+H+vjlqWV5fq8cY8uEPVOeus70VB/AbLU6z8pKkwydnqJHo5PzD2BSWDP+hY1NzVLf0GR8DvE9rFYfxKpqPXZy1BnbKb7v1HOs2uEG1KD3NykfsHg3QKgARtpGSY7ZAgAXh0pl2j13YZE88tQncu1lx3R4IxbphGSaNWtW+/l7773XhBcn4ibXCGAXKLzKEUccYYSEMHodky5pdPXABE4FIg+w2ptS1/iCNfp9j34osz5fQ5+MicLX4gQA1awOywAkimb88waqqwsGlaMK+9gMK3PMtVgq9XGexlbKOFsriJUZa/adsq1YDzWA3bajyrjslJbVGICrUTcenmkFtFYws6z1nXYOiw/BPgETgPZAXS088pCRu9320ksvdQAr64Zly5YJB8pydkgnMCbmC4RwsggpDGkM49BQxB4LP/3pT/eaGZAHWKFaQxJdI/LB48/Mln+98IWRPGJlPwU4NSpIAYb4441UY8lJ4wfLpAmDZcw+/YxLC1JFvIl3chC1YWD/wGGOcLyurmkwluwsOmxRf0iMQTfoSil6vu3FVXqtxtxDGXUe2iqVKRi3S2YOC4ZtG1Pw7583TS67YFpAl54vvvgiZGqYJSB9cWAcShTRiy++WJCuWOonEidbvy9YsCBgOgDvXXfdZe4PeMMeeNIDrC5QqV98uV7+8OB7slAdka0wLNHMditINRtbIySoKROHyLSDRsjkCUOMv120JJJo5jlQWikauRJA48AyXWRoh9uYUuLATZSJIrVoX7ehVC3bS2WzglqxnmfaiVTGdJXpJemZKSzo3UaWVIX9Fcaj8CkYufG/Ja75o48+Kk888YSwfx+bvRAeeebMmfJ///d/8uyzzxo/Qt4FmGF79ctf/lJOO+20YK/fI89bNXGKlu7VYCXEEpXRYtIkdgLzKF4cIDLCI099Kk/8+3Op1elQNCUbpntICUgWgwf00pWu4XL04aNlioIUktXeREw1S3QquWlruawrInxOsXH+3rilQldddxq9GVNjdG7o7M4+bbJcefF0DWMTmk8LFy409lGENY6EiPD74x//WM4//3wToQFJi9DII0aMMMfcuXMF84fi4mLp06eP2ZziqKOOCqvQjyQvsXyGaBSHHnqoqh7ad6QP9LoT9OTbHmAFYk0SnMMV5M4/v6V+f+t16tGqrI5GtlBUN6mSm5hRB08ZLicdM04OmVK414GUE14ikW3TaWTRxjJZpfWxWoHs+CPHyrF6OKX//Oc/csMNN8jatWudPrLbfUT7xaL9kksuUV1iitF/XXHFFUb68r95v/32k5tvvtnoxfyvJetvD7CStWYc5uuVdxbL7x941yiRo6WrYiUO04MRQwvklGPHm6NQv3sUew4gAb355psCeH344YdSVlYW0UtxeP7Wt74l999/f9B471bCP/nJT+Tuu+9Ww+HkD1rgAZZVa13skxWn+3UF8B//+dzknJW5zhLTPmj82IFyrk5ljj9qXzU5cOQd39lXe88H4MC6deuE1UPA69NPPw03DQqQgvNTgNvf//73pA+j7AawPKW78/qP6Z2sYN36h9fl/U9WmggKnVV0M+3DRGn/cQPl/LOmmmkM+hePEsuBwsJCQfpBN4VLzZNPPin//e9/ZfPmzVHPGIp6Fgn+8Y9/dAlJywkDvBbshEsxvme5Rsa86Tcvy1INSdxZWyaW9hsbW2TsyH5y0TcPlpOPGe/5E8a4/iJJHmNRbK040DkhcbFK+OWXX0aSXNBniKPF7jt//vOfg97TlS50fs7RlUqbhHn9bN46+dGN/5blqwGryMcPHUiNYWV+Xo787IfHyBN/vkDOPGl/D6ySsM79s8S+fEhdn3zyidnV+fDDD/e/pVO/77vvPiGk8p5AHmAlsBbf1oBwP/3VC7K9pKpTsdItPdVZp0yWfyhQfe+8w4wtUgKL5r06Ag7gD4jVO6YKbDCBFXy0iJVK/Bm7Ou1VgIXtEXvjcWAsiVSSKGIl8KY7X9a9+xqEyJ+REPoJbLWY/t17+9lyxw2nylBjMBlJat4zycIB7B7xM8Ro9JFHHjFO053NGyuT1113nbb9ps4mldDnI5+DJDTbwV+OpTKbKbBvHgcuGcXqplGhmw+wTRTSCECFBTOuFWwJhQFgf91FBetolv2HDu5l7JSCv6VzV17WCKAzfv+6sYeyR7J0kyrlwD4LaeoH508zvnxunvfuTX4OEOv8e9/7nuAo/bvf/U7+8pe/SE1NTcQZf+ONN4yC/9xzz404jUQ/uEcAFsZ98xZukNm6RdXiZVtkk/qQ7VQ3iyYsZxWcWHFr3TtP2W2ZysJ5vYaUwmoan4AYdk99dBuokSP6ysGTh8lh6noxWn3ocJyNBr3x/lKzGkjeIjVbwDJ7tObv+iuPk8On7hONbHlpJDEH+vbta2yq2O7rmmuuMdt8RZrd3/72t8Zn0eHGD5G+JmbPdVnAqtftzz/V3V+IrvmFblW1Q732W3wt6g+XokDQtqmnbhbglpC+tqlOiY1EP/h0pW6ZniHjxwwwFuHHTx+rOw1Hvt3Zxxph4Ve/f619Ky23ecPwE0v100+cKD+//Fiz67HbNLz7uy4HUMa//fbbRkH/z3/+M6KC4M7z+uuvGz/FiBJI8ENdDrDwxn/tvcXy3MsLZKnuSEwHNp78Oj0KurumCybj55rSTbe8Sm8FO+yZ5qr09sWC9fKYRpc8/YSJ8s3Tp5jt0V0kK4tXbJVb7npFI0M2mi213DzLvViqZ2Wmy8+/N13O191aoiPvuc2Fd3+iOUBU0ccee0zy8/OF2FqREOYTOFZ3ReoygIV08cb7S+TRpz8zQMVOJiir2U8vlsR0stXpOMXEXPrL4x+ZUMQXffMQYzmOHiwcbdVYTdhZEREgEgdmLODZpfhXPztFDjuwMNzrvOt7OAfwJ/zDH/4glZWVBrzcFveDDz6Q1atXy8iRI90+mvD7Y9vbo1S81euL5ZpfPC833PGSLNcde9lYIdKVtc5kCZ0TsdIJFvebe9+Sy294VsFza8gkAZvb/vi62dcuErCq06kvERQeuvvbHliF5PTedRHQQsKKxPSBSKiYTnRFSnrA+u/rC+XSa5+Sdz5eYUIBJwKo/CsW4AI0UfL/4OfPyPOvBrdOfuifs2SmuttEYhSKcv3YI8bIfb8512yf7p8P7/fezYEePXoIG6lmZYUOcxOIS++9916g00l/LmkBi85KeJVf/u5VEyUykg4fa+6zokjM8Rl/eE3+8Nf3jJ7J/s6PZq8220Bxn1tCssJS/e5fnBE27pLbtL379xwOEEfqnHPOcV0gXIDY07CrUVICVnlFrVx32//kn8990bbql5TZNHWNtIUt1d+f/lRuVdsqgAYisuXdf3nHgJhbR2ZWQL/59cky4+eneNt1GW56f0Jx4NJLLzVxskLd439t06ZNsnVraHWG/zPJ8DvpkICOfu2MF+Tdj1aYLatYtUt2ApBwWn7htS+NvgrAeegfs3RHm2LXujYky7NOmSQ3X3NiRAr6ZOeVl7/oc4Bdc4YPH+4qYfRY27dvd/VMMtzsfq4Sw1xjjX797S/K7LnrYrK/HjZWxlrUlEGRsPV/1ErEVluvqBU7oLto6RbXkRcAqxOPHi+3XHuia6CLWiG8hLocB9jjkBW/NWvWOM57i0b1WLlypUydOtXxM8lwY9JIWOwzN0ONKj+ZszYqYIUZhNkaSn3tmKbhP4i0hmEp++NhXIp1u7lHgcK6h3OdIdL+5Iu1xg3IjXTIauLUycPl1p+fbKztO5MH79m9jwNYw7slIkRceeWVsmjRIrePJuz+pJGw7lf7pjfVbaUzW6xbIAUY9SvoYfbOG62OwYVD8o2hJ5E2Wd1jCofzc41uylmq+9tt0l1TiKFOXCr8DzkP8ETq55fmchssjEKHDuotv77hNM8nMGFdIXlezG7RTz/9tGAvhbMyFu7f//73zUYTwXLJiqFbKikpMVuJEeDvoosukuuvv16GDRvmNpm43p8UgPXWB8t0Z5jZEcduagWqJumrbjOHH7yPia7JXnp98nNcMRMpi80GiPr5tuYJ+y9Lqe4qIRc3k/dMXUX81c9OkiFqHOrR3s0BwiZfeOGFsmrVqnZGsJnqM888Iy+++GJQXRUW8JHSzp07jWM16d96662CEj9ZKeGAhc/e7x58V3foVZVSqjsNO5M3FNx9C3Lk7FMnyzdO3t9IKpEyG9OJ/fYdaI6Lzj1YXn13sYmvvl53TcE0wc0Uz2keiLpw+YWHq1HoCKePePftoRxAsrrgggsC6qIwQ2CnHECF8DP+FMmU0D8NNm0lOgRhbdiVmq3Dko0SrsNi0wWmZG5dbJBMmjQU8KnHjZcn7v2u/OR7R3UKrPwrhm3Zv3PmgSZyJ5s3sGca74wmobebqhEhLv7WodFM1kuri3Lgf//7X0CwsoqD0zIhZgIRYBMtwrH65JNPNtuJRSvNaKWTUMBCOU20hYz03UeMUAXEIRld1y90Ne13vzhTCofmh7q9U9fQhd163Sly09WtK3fovqJBKPezstLlmsuOicgKPhp58NJILg6wo044Yrfnzz77rMNttCU2bY0msZPNqaeeKvPnz49msp1OK2GAxVTo4Sc/USmp2dVUCwV1n4Jcuee2s0zUhE5zwGEC3z7jALnjxlPN1LA5CpIWq4Knn7Cfbgc/2GEOvNv2dA6MHz8+bBGxn2LaxvTRotLSUlm6dKn1M2qfmEmcddZZsnjx4qil2dmEEgZYuK3MXVjkKpY5oWQIrvenGWfp1uqFnS276+dPPHqckbTQtHXG/IGpZX6vHLlQ9WQeeRywOHDaaafJuHHjrJ9BP5csWWJWDdm2HuK3HcACPTht2jSze/SQIUMCXQ56DqnvO9/5TtJYxScEsOiwz740XxXtznVCPMNq2h03niaTEiiVnHnSRAWaqYKEFCk1NDbJCbqh6XA1t/DI44DFgd69e5tQyKmp4dfCWDnEjgpiZRFD0FBEPPff//73JlopDtNuQstgp8U+iuhxE00JAaxlGniPoHhu7JXQW1156fSkCAl8+YVHKGgOMUanbisQu1QC8Z2hwOeRxwF/DqA3+tGPfuR/OuBvtu7Cduqtt94KeN062bNnT5k8ebL5yR6FV199tQG5n/70p7rYFR4cefD55583u0hbaSbqMyGA9fZHyzSYfr0x4HRScKSZIw8dKed/4yAnt8f8nmxVlv9EI3+mafBAt4bxxHLfd1Q/mTBmYMzz6b2ga3Lg9ttvl4MPdqYuYHOKd999N2RBJ0yYIEOHDu1wD2YQBAFko1WnJhEo/J0sDHR4UZR/xB2wcIVhdRCDTCfE1ly5ORnGbMGt6YOT9CO959ADRsj0Q0eplOVuatisLkKHqv4tmcoSKQ+852LDASSiv//979KvX7+ovGD69OlBozkQKvmll16SwYPDL/6gJ0v0DtLOUCMqbGtNBNeXtRtKHXdYbJVOPmacjBs9IIq56HxSGJF+S2O7E1DQuSZOfRnVbWfyfu4Un53PrZdCV+PAfvvtJw899JAuSqV3Kuu4oZ1wwgkh0yCmFlM+J+49bILR0NAQMr1YXow7YC1ftU2qHU4HWYnLycqQc79+QCx5EHHaB04aJqNG9BGkJidkSYuest0Jt7x7zjzzTLO9V2c4UVhYKISfCUezZs1yFNAPsEqk8j3ugLV6fYnj1cFGBYL9xg1U6ap/OH4n5DruOkzvzP6HDnLAqmjP3Azp1dN9SFsHyXu37IEcQEF+0003RVyy448/XphiBiOEAnRTP/vZzxxJToBfJCGZg73f7XlnSwRuUw1xP244Dbri170Ba6aO5D+1Yhv2qSrFsMFpstJ++w7Sim7dsNWe/w45bvuBHVnrjtORbU2frDzw8hVbDqCEr66udr2tF9PBUOGTMTi99tprhWgNTojpqdMVTCfpRXJP3AGL7eCP0N2KTZgXWwS9lO7d2y3e0Q9119+4wRx6YGEk5YrbM1NUH3XDj48zfoYAEnlubva1fep3tY/hHNcaFdgGDcjTSKJxZ3vc+OG9KPocoC+wotfY2GjCwTh9w9ixY01omkD3f/LJJ8aOi41VndJtt90WND2naXT2vrj3nB+rLdWeRP379pAfnD9tTyqSV5Yk5ADberFCxxTuwQcfdJTDc889V7Kzszvcy0rfn/70J7nvvvvUtKimw7VQP6666ipj8xXqnnhciztgxaNQ3js8DuyJHAC0ABokrmBRG6xyA1Tf/va3rZ/mE+t4dGJuQinzIMH9kPCYYiaaPMBKdA147/c44IID1gaqWKiH2qr+uOOOE7szNREe8AkkWJ8bAvT++te/BozB5SadaN0b91XCaGXcS8fjwN7KAUCLad2MGTOMtOXPBzaluPHGGzucBnTcgtV5550njz76qO6xkNkhrUT+SKiEhbd5ZWWlsf9gxIDRTozX4sEwbE3Ky8uFPIZzLI1GftBNIHITTRIe5OS4C+8cjTwQ47u2ttYsg4daCo/Gu7w0OscB2sqvfvUrs+vNAw88YCI20GaJ9gBYHXbYYR1esGHDhg6/w/1AZ4WzdGcNV8O9x+31hADW7NmzjesBwcFYWmX1g1GDjjpmzBj51re+JWeffXbA0cNtAd3ev3nzZmNhzAYAW7ZsiRtgkU8aIcANWBx44IFmFWfSpElui+D6/jfffFMefvhhs3sKAM372fjgmmuucRTuxPULgzwAv4llvmPHDsP3ILeFPQ3o5+fnG/85fOiibTcEsLNFFhuRAvAMNpEQdZ2XlyeEfGHzh0gG61NOOUU4iJPFwEp6gYjoDE62p2egvPPOOwXASkaytGinaOZeDZZBGsAXX3wh0eg8OGoyL66oqDDSBApES5kHw9klhAZAZ4FxXI8XsRsu1sUAKSMLIGrlLV55oOwcbCM+aNAgQVE6cWJsIjswIrNUbS2ZU2bKSz3wfvzLHnvsMTnmmGNiWnwGLTY/wKcNMGAAixQEyChloO7ofAABsaCo1yOPPNJxdIJABQbMcTb+17/+ZcAKq+/O5JN3kE8U5NQ1Ds+nn366HHvssbut7gXKj5tzWLJTj/A2GBHRgZVI+BRPIrop7kFhLOjxL3o7roAFGFEhgFaoUY8OQ6NjREDSiBdh8fvrX/86opEuFnnEWPCyyy4LuyIU6btZaSLESEZGRsCBgQ5Jh8d/zN/bP9J3Bnru8ssvN1ItKgH7ABboXjfnaEd0AjopephDDjnExHWiDUYyEN11113G6hyAieZgBuhZ+STd/fffX+AJSnLqJlqEASpt3J/gO/sT3nDDDUJMrniTG8CKn/iiXCA0BdJLuEqg0SJqL1iwIG68A0xn6m4h4fIWtwzpi5B4Pv/8805Nj4Lll6kvkhXSczAplvevXr065GpUsPSdnl+xYoWw+QLToWiCAO+nXJQPgAGgPv74Y0GRjMph+fLlTrNo7isrKxM2ZyAtpnKRAF6wF5IWaTKIw3PiswNYRCD1j98eLA0n53/xi1/Ik08+aYB74MCBss8++5hwyx999JEAxokAKyf5tt8TV8CylLpOKztc2Fd7QTr7HQU7OhQ6TbIQeUGfw5Qp2vTUU08JO63QUUIRksl//vMfcau0DZWm/RqDErwPBpr2ezvznTbHYER52SrrpJNOMnv9OU0TgINf8WgfgBZ8B0iQBjFfQFqMBgHYADegCO8feeSR9uB+0Ug/1mnEFbAQe93M+ZGy4kVMv9BROAXTeOWLPLmxSHaSL/SHSAtIH+GIDgqQc38siLy4aROdzQP1iyTDQMBuyujwnIABJgG033i2D0CLPsBUDR++aLUDQJs9ByNR8neW/519PqkBy0lD6iwDrOfpNPF8n/XeUJ90Dku/Eeo+t9eYgiExOAEs0mbERyKLhaSXKJ7TaZHq0FkyVQoHmtZikFted/Z+BgwkQwL6MU1kANubKa6AtTczOpKy04noWNHUq7H6hzGgmykYeWAZ/4UXXoikGEn7DDxAimGXYwwrk5UYuFjxZLt6VlP3ZvIAK4lrH8DCriaaylBWaFmVQWpyQ4z0mDjEc5ruJn+R3gsYAMgAQbJtGupfJqay999/v7z88sv+l/aa3x5gJXFVsxxPiJBoWZ0z/WKnFXQxbgmAo0OH26HFbbrJcD9gjOKfJf9QdkqJzivgSh3OUJcc8rs3kgdYSVrrNEwOjGxpqNEgjH9ndsJ0ozOAF438xzINpobY/b3++uuxfE2n00bvyD6BTzzxRKfT6ooJeIAVYa0hpbBqE4uDaRcNE6NOXJSiRSxhk99IARBdGkvtbNyZSGKqHOqING8AMrosFOzRoFB55FqkRNtges4K695GoY1w9jZuOCwvDRqfR7ZI6kzD838dQIKeAqM+3KDYOSVahIHmq6++2ikFPvkDTJlWHnHEEdHKmut0wgEu9cPUjo6NfsopMe3FUBP7JCcbN4RLN1w+GfTwJiCP5NUpcS+rvOgjzzrrLKeP7RH3Oa/NPaK40SkEHQKveJbDuwo9/vjjUlxc3GkfNaSsN954QxYvXixs0BlPonPffPPNZtsqpKFghH6HEMDPPvusrFPvCqZ7TgiAwd6K8nUGsDA9wMaLwHfB8mm9ixDF5POrr74yg0k4kLPKQbr4mXqAZXHE+wzJgUgU1yETjOFFogqww280zCMwBcBNhSkJ4UfiSXRSFiGmTp0a9rUnnnii8cO85ZZbDCA4XRVFAY8lOO9yY/phzxDPDh8+3FE+v/a1rxlw++1vf2t8RnmnE9BCymK1lygNXdEA1M4vN989HZYbbnXRe7HfKSoqippbCcDHxpu4qsSb3OiXiDaB3g5Jx6nBJdMzQtzgRtYZcjOgYbZiOVY7XaUEWPEHRYLcm8gDrD28thmB2cbJjY4kHEvoLITiwZE22Ym84n4zYsQIR+YclgRJ+eJN7A1IiBmmvuEIKYwFFA+wwnHKu96lOECMqSVLlkQVsGAAUyziQnUFe6C+ffsavZdTIEAa2759e9zrGZ6ik3IqnXEffp57E3lK9whrO1L9RoSvi+gxOihuOE50Im5fwNQJd53//ve/cskll7h9PO73uwmCiA6K0N2JIBYynErDrFC7NW1ggEF6JBIKukhWfUmHsDn9+/c3cc+YSrtZXY0nnzzAioDbgBUVjtW3W7MGnqVxEGGSaUosCSNR4mm5Uba7UTZTFgCRQHNOV+JiWd5QaeMt4BS4qVOnuqRQ74zkGsH0AAvy4CS/TqRGpDBWFGmvrO4SqQKgQkKz2i/vAihxBaNdEiKb0MsEPXS6YBFJed0+4wGWW47p/VQgKzSI71aFO02GhkFHp2GyUQA+bNG0t7LyQb6wl6LjocdxQuQNJ1unxqXwYd68eSYi6de//nUnr0jYPZgrOK0r+JAoCQPeAyRO6yyUNMbUlqiyGMOy6GKVi7Qpn38Z4Q8SG20bezSenTJlilx88cUm6CEDbaLJU7pHWANW5dNg3Bw0Ep6lAxFIjsYQi7AtAAmuJk6lK4AN4JwxY4YrS286F8CIZJbMhB7PKTGgJMpUAINQJ1ITZaEdBfMzpU1dcMEF8n//939Gz4VBMlKw1f4C8YL0KDsDEeDEb9y5CGvDgLR27dpAj8X1nAdYcWV368ushoGUReTHWDgUEz8JUORdTghzAfwWL730UmMU63RKBCB++OGHUQ3l6yS/bu5BaiAufShpxEoPKYMy9evXzzoVt0/An1hlgIYT4r4BAwbsdiuA9+Mf/9gMiACPvyS12wNBTtB2AC/AbqaqF9hWzKmUGiTJTp92xplOv8ZLIBQH0DNFk4jDzuqgU+mKjoKh4znnnGM6NVKfU3snGjX6EOydkpWwccI1yUnHRVLs1auX0THGuzxIqhitOqk3gAMwKiws3C2bWM5jJ8f0PlpEnpCwEi1Je4AVrRqNMB1GyWgbYGJ3xbK805GaoH5szMAqEcR3p3ZL3E9jfu2112Tp0qX8jCk5AR0rAxh/4kB+3333OVYcA95szoApRGfIqQ6Kd6C3uvvuu830zWmdkU+kK3/Aoi4BPjfvd1JO3rfvvvtGPV0n77bf4ynd7dxIwHckFBpstAigwrIdUd4JMWLSObEGt4jfrPwRPtiJopVOhs4Edx06XqyI92CFTlyuYCM9/MQkgc164QMgCqBy3gkhWbIY0pkOTz7ZtCNcPpFMcbRGIkJXxJTVKWAxZUchjjRop2XLlpmVQCfTX/tz4b4zUODsn2jyACvRNRDl9+MzuE7dNdA7OCFGZMBp1KhRHW6/8MILzTQP/Y+TTgQoPPfcc3L11VebDVg7JBalH4AwPneAYjBdCsBEZ0aPQ6d1Y25BmvDt5JNP7lSOeSebdmBYGyqfSC2s5AGO8M8NUU7MDvyJxQU2VAlVbvJEvUMAEWkB1OSH3/DNDvDk8eijj5bjjjvO/3Vx/+0BVtxZ3vGFNJ5o6RpoqAR2czptst79ve99r2Om9BdTQkZUlsSdSFl0Oqa2bFZx3XXX7ZZetE4Ek6zs6QOwTgHb/hwgh3TVmUgNVnrw1kleI8kn4MK0FQdvf8K/MNR7yReAxLQfx2sWF+AX02cCA7KyjGQI6DNAAGKsmKJwdyq1++cpmr89wIomNyNIi8bF7srRIOJdWWFKnKTHKMsoHayDEiLl6aefNqOxEymLBo1UwW7VGCDGguwjfyzSJ+/R6pixyivA+t3vflcKCgp2YwHSUCjiWbasf/jhh3e7jWCRmEEQZ4vpPYEakdRQDQDkyUCe0j3BtQAQTJs2rdO5YER0s1LHSMs0hClcMDBiy/QzzzzTsV0Qkh12RLjrdDWiox955JFJoacJxTvqmRA7P/jBDwLeFg5skYRZRcaQNBDRJhjE2DyXmGIcSGPJQp6ElYCaACw4ULYDVtHQDVihi93oQhD1iWHONCAQAWTosJxOMUmDZyx3HTd5CfT+eJ1DyoUXRHVI5jzTZiA2y2Aj1EDE+VCSHXW5Zs0as6M0EvRRRx1lJPxArkvRkvwD5TPScx5gRcg5Gjnz+0iIUQ6jUXQI2Ah11qqahox0xehL2k6IRk3omT/96U9BFcOkQwMPN2rb38e9RNF855135NRTT7VfSsrv8I5pEmB16KGHJmUerUwxwF111VVy7rnnWqd2+yxUu6xwK4RcZ7WVcDYAFeYsmEgQ9pvwNkz/kOKSkTzAiqBWAAacQtlC3Br1nCYDUABW0XR+/vLLL421vFvpgLxEovQNV1aAHFsgVtuCTTfDpRGP69QdpgU/+clPzBGPd0b6DhZUMOy94447QiaBrRRmKYTDDjV4MRBxANbrdFUZqQspHQ8JAgoCXKwen3baaZ0eUENm2OVFD7BcMozb6ZCMSIFWaSJIrtOPMAXD9sjJal6nX+YgAYBzZlukiGSVWlhpA7Cuv/56mTFjRtICK5I8iyOYmdx7771h65gNTOA5u3Q7GYwYtAAuOyHJ4crEgX/ptddea8CLexNNntI9whqgISUDMTrif+ZWuopl3mnYyeyuQ97y8/PlwQcfNBKLf4eNJW/cpA1QMcVGqmJVD8ncCV2srlVM+9xK/1baSMW0Jw7C0eBfyuphMrR5D7CsWuqinxgosslEKPE/EUWjsWNmwaphMhCdl+kPB7o1FhuQWpKRUDmwaomkxGCEXZub+mURh+gKAHNnCcCkLtF1IsknmjzASnQNdOL9GPthJ+VGKd6J17l6lFGa/GHPkygCpJj60XGZxqOXwc+SJft4b1EWigf2fAKo48ePN/6P+GdGsv8j4IZHAGWMhtsXEjMSG2kmOiRzx8lrKK5615KOA7jCYFPjRFeRiMwzMlvuOuhWokFIH4BPqOkOHYxOiwfBkCFDzKoX9mQs4ccL3AFKS08WrNzkE2Cn/oYOHWq2BTv99NPl+OOPdzz9C5Y2Jgm4aRFmBn0igNOZqS/Prl+/3vhnotNKFHmAFSHn3YjoEb4i5GOMnI/r5qiJzkeoTJI3nICRAoma0FkCrIgfzwptKMCic6GjotMSNidark9O84+UdMYZZxgDzFD5BKxYkQOsyGe0vQNGjx5tYmIxlbv//vuNKQO8AbwAS7dEfTKVZlU1Ue3OAyy3tab3U+noZhCRo62IRCphBRJ3GexighG7E2PO4EZiQDKJBlmSgZO06Bzo2fBX7GyHJP/YriX7bsdIVtQf7jOJJlxrfvSjHxmTCKJXEBnCcpC2JFHaM+AZjrgPiZ6pfiICHJK/pAYsGJSMZAFWrLaqpyERNoROPmPGjN1AiY6LoWio0TsQ35iWdXZkJG+YUHA4beSEeCGgYDQ6MNJLVyCkwUQSbQRndFYW8TkEYJCMfvjDH5qQNrjcMOARlG+drjQTloi2EarPUd/UO5tYeIDlV7uMzATDZ+nZbcf0S8o8jySCRS8xhBDBO0tUbiztnlAU/+53vzNB0/xXs2bNmiUcTqUrOjkrTtFQ0ANYxJrCv4x64Xc4oqFjkPjNb34zqcwvwuW7q14nYgPAhMcBXhTTp083vocsOiDBM6XmgAA2FOmEuf7jH/9oZg6hBjWAOBqK/Eh5m5wijJYGwPrggw+M8VqkhbM/R8ei42AFfNNNN5nA+vbryfadvNJwWNFCMrEDA1bkLHs7VbYDLEhrVkTRzpYVpTCrV7jfhIq7ZL0HYGXwwWexs7GmrDS9z8AcoK5vueUWEwEW3R3uV4QcwiGdTSlQmKMvs4g2xsLEeeedZ6QrBsdQgEU7tLdFK514fSYtYMEAxNNQImokTGL+zVSOUYfl42Qmyo7PFw7IVmRJYha9+eabjiUVRsRx48YZZ9dolRUwxXE2mNN0oPeg1wFo8Q7geY9iwwGU4qwOMhW0gIWZAFI2s5WXX37Z6ACxRRs5cqSR0nH7Qa/1wAMPhO1vCBJODVhjUcKkBqxYFBiGAwBMa5IdsOjYjJB2wLK2h3c6HQWwkNA662DtXxcAz+TJk40+xMnUlKkIEjN6E6blHkWfA1jG//73vzfTPP+BnraERI6u6s9//rM89NBDZhCkP6B+QDeFdMbvYMQCEwMns5RE0V471EV744dYVCAjJIBjBWWjUbE66AQgyA/6CZb22b4r2kTjx8SAdzghygL4onz3KDYcYMcddmAK1T6Y7lF31Afb1gNgbAcHwIUCK3KMlIypBCYjiaK4AhZMscTURBXYeq+/4pB8JUverDxaSm1rCsWKDkAbSsdgPcsnIy6hSKJltGlPm+9EqCQWPA3ZCZFvdFldkZKtbQTiIYDF1M9JXrmHdkWdWO0rUJr2cwxOJ510kqP07c9F83tcAQtk9xdVo1kYN2n5VxJTFkYmQCKZCKW2pTNwClTkH/Gd5eyL1RE2VsRISwgSp6YG8Naf7+FGdXve6WRu7rc/29nvvNcJEFjvcVNX1jPR+Iy2XaCVJwYlpHUGqURSXAGLuS+dLxlAAQtjO2HUSAeMVYXb3+X0OyMaK3uWCI44fuCBB7ZPEUOlg3RFqNtYB2JDP4ahqxO+cQ+uJ3ZCJ+IPYvbr9u/cF21dnD39UN8JdAdoOW27wbaQD/WOzl6jvmnX1H00iTKjmvj5z3+ekA1m7WWJK2DRsHHItHQy9ozE8zsN378jI/1hpxLtyu5MuZBcCKGM9AfxiXU9dmShPPEBBgaGK664ojOvd/QsS+SMuqHqlAbPShS2W+eff36HdAs1QiZSZDggsMrE/YmgwYMHG0t9J8CMpM7gEm+aOnWqPPnkkyaGFSoPp1P1UPkkDdoa1vLB4siHej7a1+IKWIjJROlE0qIBg9pIETSCeBwwH8UvQckOP/zw3XiJJTDuMCgh4503e/nJJ/zBPgYDQDuxwoZNDRsm0JDIpz/RWAGRYLvh+N/f2d80ZCQlfwU8IMQAwCe8Zcswf7stTC4mTZoU1hiRMgHeLMUnghhs4bm/7tM/L9QJZQI8EkEnnHCCsY9jpxvaD/mhDpwArZVf7mWwpKz0VQyYWX1M1DTXyheflpkyOzK+ar9g/44ozM60NKxo0MKFC03jZUUDu6hAnS4a77GnAbOZWmHxjfFcMD89FNt0LCx/WUGJt8SFjo/p6cSJE+Waa64JCjo0RHz0yCuuLzQydCzwEjDGb4wwzPEiomES5A3QQoLlE4kQkCF2OB0pGLEPHtIguyBb5aAsAB0H6WFCgbV8IsPCEK3g8ssvN20DPpNHez75zn6B2DsdffTRwYobt/Pbtm0zg9vzzz8v9DnMY+Av/PTPu8Vr+gnTWUCXmFos2qC7iiWxEEO/9B/w/N5JA3o7IYBlZYROx9KqU6Wt9ZzbTyoD0EUKcOq5T57KyspCTnXc5sPJ/eSTBmMp2sM9Y5k6vP/++4aXbM2FUWcibGUINofRInXKgECDJ6SLk4UWJF/2w6Px4ioC/wE8VjjR2x177LEJ01/Z64ABDHuyzz77zESiYCpMncFvBnSC5yXKz86eT/t3QApnfVx12LcS4KWOAF3qhj7BAg2qBmLCM1iy+uuk3uzvifR7lwGsSAvoPedxwONA5znAQI6klWhyA1hx1WElmjHe+z0OeBzYxYFkAKtduXH2zQMsZ3zy7vI44HEgCTjgAVYSVIKXBY8DHgecccADLGd88u7yOOBxIAk44AFWElSClwWPAx4HnHHAAyxnfPLu8jjgcSAJOOABVowqAbsoQhKHMYaL0dvjmyx2PhgnElhwbyXqGWNdN0EN91Zedabce10Av84wy82zt99+uwlWh8Wzf3gXDCNLS0uNcSVGh12dsKDGKp9yYuAZL4PDZOIbXhHsAYjRJTv7eBQbDiQEsLBiJuohDse4ZOAqYBHWt+yhxn527GWHP5Q/vfjii2Z/NPtz3MMoR4MhTbvfEx0KXyh8CHHAtROW4mzDDYiQHsZ0WMTj+Y7vGLRp0ybB9QRrbGxXuIcoCt/4xjcCuitxHQKM/CUsLKMJqIdbEq41xDi/8847zb24sPhbuCO9EFp43rx5HcpE+qSNZTXuE3Zap7ugwF/yZ5XBuo6VM9fwE7PscPCTY+ss+AMRFZRdeex5HzFihNlEwh4P3EoT30d4B8+tslvX8GaAd+TJ4i9lJC685a7DLizUD3wH+GgXdiJ93F2w0rbXK/eQR8L9+keBwLIbtyXC3+DUbqeVK1eacMDUhcUDnJvPOeecdqd46odY6Pby4NDMRhrc60/kkbxZ6dmvb9261WzwgB+kv28o9yGZ4h+KvyXuVDi4wyNcyPwjy1JeyoWrTSBeEK+KercTYbYJf0w7Oeyww+yXZM2aNaa/UU9W27Z4gdU7hIU8O3jTFi2iLPSlQP3TuieWn/gS0ssCHtrxfOrnpXUXHVL3DZ82Xp86wvpUhO6QqIKDT8HA5ENF7A7X+KFM8x1zzDHmulbYbvnVzucrLi7u8JwCgrlPXSZ8K1as6HBNG6ZPXUB8lNFefg1j4vvNb35j7tWNIMw1+z1auT51x/Bpo+6QHj/Io46yphwKvB2ua8PwqX+deZ9Oo3wKRD6VSHzaSX3qU9fhXn6oJOZTsDD8gmf2PPKdtPxJQ76Y+w444ACfAnKHy7pVe8CywDeL37/61a92u4d3ayP1zZw5s0N6/FDpwqdA5lN/MJ8ORh2uq5+jT12NTBnhmZV/7Yi+6667zvDqrbfeMue5T4Gmw/P80F1gfCq9+ahvexpWWgp8HZ7RTu1TADZpql+lD57b6S9/+ctu5SNd7Xw+HQzNrTpYmnuoG+s98EA7sU/Da9uTM9+LiopMfVPv/qQDsElDgcCnwN3hsoKmTwcVc/2yyy7zaRA+U0YFLJ/GWe9wLz/UP9DkMxgv6Bt2oi0qyJr0daccnzrV2y/7Hn74YXPNv20rcPpeeOEFc+/1119v7vHnhbpf+XT3pg7pRfJD/ZRN3Vp8DvJ5vJ6P/76E+GIxWuInBmJr4zH+ZtoYyI8Z0bimDDS/A/3RxmWeZ9Rn1FMmmYNRDl86fPEsYvR+/PHHjb8UvoFIK3fffbd12TxnhTdh9MHxGCmEUQnJBwmIcCHkiVGPaAl8V+ATbaRy8803mw0tCh2GPeFd7ITDFIK84pOGRMFoCT/8iXNMsXiOEY/Rzyov0ijShZ2QxF577TXDg8WLFxvd0sV+QfwoDzyiLHxftmyZiWSK8zKSD7znnX369DH3kAfLB+3GG2800QCc+mSSV/jFJxIcaSKxIgHdd999JoIl1+EBn8F4QD7hAXH4kQjtPEAyshN77uGXSBnhxyuvvGKkJ+se2g9lJHYUPID3bMLA9liUTzt9Ow94Fz6CtC2kGqQz+ASPya8TQgqm3pC0kKrZ1cYidh5CgqH8Z555pikX3602ad1nfVJuqz3iCO7PC9KwE3sX6IBgeIHkzOwEqdMi0oMXSLfwgu/wgjqCF0xvaQsczCrgBXUEL5DO4AUSIvmNJ8VNwlIHWR+jqzZ4cyijzKiijDOkIYB96h1uJBBrxLeu8anM8qmexCC+iszmEueCke615tMKbn+fOnj61q5d2347oyUjuwKVD6SHGNG1Y5iRTqdPPm3wJj9IETp1NPcgDSGVaAX7kBjtRH6CSVja8H26zZhPp5xGakXCQ5qDD9pw7cmY70iLCspmBGI0hEKVVx2fTXnhsTYiI/XYR1UkLG18RlqyJNFPP/3Up0BintOO7tMpieEv0hv5hZ577jlTb9rJd5O2Q0lYSAlIotoJfdo5TVo6cPh0mmbeoYDv08gY5jr3IZH5E20C3sPrp556ylwOxgPOa4c0UrPFA+pCB8r2ZHV6ZCRW7fDtEijlpg54RgcRn4KKyZ+CTftzjz/+uOFpIEk9lIRFArRVJDSNgtA+A0AS1C3tTTsjj/CafJAH6kMHnPZ3W18U9HwaEcLUoYKfOR2MF1xE2qb9W7zQ3aI6SJy6GYVpWxofzke9QNSHRjYxZUXisyRudWRvb3sad8vUGfkMVGcmIYd/3EhYu5RHcYBHJAKkK3RYePMTtgK9EPNrLZurHDC6saEBIwAj4tFHHy2szNkJierRRx81pxgZGN0ZRQlTEoq0AbSP4MHyxT0WMWI7JXRHrKgxQqMnYeQKR5ZEiS6I8lJWdGyM9nZCCoInjJJslIlESOgWRtVQZJUxVDmse0gn1H1O3kNakaSHZHHXXXe18wC9FVKunXCktTbqIGQJ0gpSbLAVTCsf1qc9Lb7bz9u/u+UBYWmQUKgzIlpASD8zZ840ktqVV15pJD37O8xNQf7AC2YKVns47bTTzC7O9tstaZt74YWClgkTRdsLRfa2bb/Pnrdg99jvj8X38L0lim9lyZdA+XQoplJM166++mqjQKehuQl6hhgP+Kxevbo9hzQAwMsilpmZehBW95577jENhU+iMtKAENMhGh+VgZLTmhIyLWRKQXiUdW0KYwL7sRklDYApIYCISO42UB7PMwV20+gpL42daZ5FRIG0R7ZEGUt8MWIyAdR0AjovgwSKWEts590sRKD8BzDhEQMH/CDulEpc5rxKokJHoHHyXgYagD+SIHq8h/0g4T/TDabc1Av15bTxwwNifzHwWUSd2eM1MfhRNqZLKhEJG4MS24zzTO+tAYK0mJbDF77DAwYTwqow7UQJzr1MlxkceCcDAp8s7LjdPZwBGqU3Cx7k62KdprOwQV4ZgPyn9lb5gn2SZ/Jm5wULFqhJLKLeaaOoElCas5kufRBeUK+0Q4j2wKo1CwpWurR10mIaz16H8II6gxco/i1ewOdCh+oQK1+d+YwbYFFI9FXoAhj16Fwwm+9E14S5bgCLZ1n9obJp8ICgfQWQ1T8aBIymEm699VbRKZB5HzvPUIF2XQJMRPdhdR6AC1ClgwIUgAvvpMIBN34T9wg9VzwqjHezFyB6L/JI/uwrgACoKkkNKKEnpPECuoyqDAaMqqwEQuQd8EF/Yv3GJIHVKYIcUlfcA4gBeBCNms7MqO5Uf2UebPtDegSBJG3qinpBOsAM4u2337bfGvQ7PECqogOSDhKzfQWQAIDoq2hTdDhW3Vj9hQcMlOi14CFEflglpD6t34AQ5QNI4TH3ACh0WAgeAGZIeU71V+bBtj+sXj/77LMG/O+44w6Tn84IXQAAMWVJREFUH9JhYHGbHrwAdMgPvLCCU1rvs6RteAEQwwsGCnjBgATPLZC0eGHVA78ZvOAF7Yw+yzkr9hrvgBeEGafNWAOh9e5YfsYNsBjlZqr4S2OFwTQgiIYL0rMjLUxGbLaIe/0JxkEwkc6LKYAlqtJR6WQ0ONW5mIZBGjRMOidMBrw4h/ITCYXfFiFxAEI8z0iCGA2RPgf5ZJSiwxKwDQUnI0y0yBrx/NPj3ZSXzo3EYJUXoKeh02BUv2UAmd+MlnQyygm/uJ8Bgfxav3mG8tKg4TmSBkBgER3W2nUHHhXqKIpyO9JlbNLj/YzaTFOpf5S2BJKz10EwHpAveMCyPeGfLR4AKLQhys0gSKciDaR3QNriAR0cyQLAggfkhykzMeZ5FnMC0rVLj9wDby699FLzDIAND5CqIyH4y/PUFaY7DBpM3QGeQBSKF/QheIHUZvGCslNeixdIV3xnoLa3B55FeACwrPZAm7Z4weDF4GaX3uEFfcPiBVI8ZYlnVFt4FBfAorGwIkQHQ8TE7glGQgAMBnfrdNoFE2fMmGEqAMazOy2oD7NgMp1Fl1jNczwPKL366q7IzjRoJCJ0VDQKfrMl/S9/+UtTMVQOUgfTUDrL4yqaAwKkD+kyu5kSmR+2PzQI7uGdxMqm4jpDVgPj0/4dKRCQ4Bw8Q+JETId4NyM7vLOeocHTaJgWMXJzHhC6uG1VkPIiNarS1HwC2nQCeEkDJT3/3YN4F+lwDx2TeusMkRYHdUE9H60SMZIQfLf0ORZQco8uSJgVK56BB0iRdEx+wwOmlUgn/IYYpCjvRRddJEQ85TydCmmb7/AAKYp6Q8cJPwA4yof0oIsyBrT9y8iz3MN0E91htAgpi1VipB7ewaBpl1A4B8ELJHzqh3PwgnDTSJj8ph6pVzW9Mb95hvYA6Kh5hJG2OccAd9555/HV8IJoqRg1I0DwnX5m8eIPf/iDkcDMzbY/vI976H/R5IXtFY6/xgWwEM+ZstDpYSZTATvB5Ntuu61dT0JlUEF0MAtMuJ/OjOEdIyMNlWuM0BZRyTRCFLEAE42VznzEEUdYt5hKB+TQY7HMS2dA8iItPgMRnZsKo6FbDSrQfU7OIU2QDu9DPLcaK4Z7ALCdFi1aZAALfiAlwhP7+/lNWdG/EL+bxg2g20dGtcUyHQQFP0vR1hQiVFkoLwRAwFPujZSQ4Cgz+bbSQTJmIAFgaBfozbjOgAbw2omBBUmAjgUP6JT+PMDIGH0OUhUjPttR2XVM6JxQdMNPeMD0iTSszmp/n/WduoHgAffSlqJBSOQMKuiy0N/5G7xa7wXQLOW89V7qWVcVTb5p/4HaA7zgPvRzDKwMwoU2HRMDOoCJlAs/mPrRFqkb6joQ0d8geBGqnwR6NtrnIm+JLnJCR6Ihslpj1zNZSVx11VVmNMcGiIbGdA3AsQMIjKICGPWZN1Pp9uukxT1UDnoN9Dko5C1rautdfCLJAGJMg5jasWpHo6QzByIAltU3yuFUuiIvpMlhJxok0h8NCykDcOD9KMvt5eF5pieANAp0Oq79OmlyDw0QXQJKYn7bwYp7AEQ6B8pqpg90PMpCOei4gYgt6Hk3aVkgE+g+65xVTt7vT2qOYDoeHdDSUVIOpDvyAxjBE/LP4GMvI+lRNuooUJvgXdxDzHemKPARgLSDFfcwxWcKBrgz7WPwASyRGKzOyH12QvKBt7QJJ2AVigf2dPmOhEPdY/dEfu3EAg5tl+mcnRfk2eIFFvjr2haC7M/CC9Jk6koaADL9wU60N6Rm2pM1tWYVmWkgi0yBiAUqgBZJ2J6nQPfG+pw1bGCHtWtu5fdWCo7ClAr0yBkHGDkZweAbyuE9mdCdoIthtMZIMdGNOhG8RroDkOkj4cxIEpG/ZH6nm5jucZGwkplZscobUg36uT0drOAfIzPTWaS5vRGs4AFSIAtHSHMexY4DHmDFiLf+oniMXpM0yUZztTRpCuUyI+jKPIotBwJrmWP7Ti91jwMeBzwORMQBD7AiYpv3kMcBjwOJ4IAHWInguvdOjwMeByLigAdYEbHNe8jjgMeBRHCgSynda+tqpVRtdXbW7DQGhM1qd2LZZUTCPCykuqtdEmYb2ZlZ0juvl/TsEdgWJZL0O/sMtjhbt26RivIKNZqsNfZDgcrb0dKr9a3B7sOIM1Ntf3ppWQeqkSWGtslMW6tqZHNltVTUN0htU7P4jKlXoBI7LUU3XckUyUpLlfzMDBmclyMF2fGN5eQ0p/73FRVtlaIN29RerUot5et2s/Hzv9/J75SU7moLmKX2fnkyonCQrnb2dvJYwu7pEoC1aetm+XD2J7Js1XIpr6o0FrmBjBQj5SKGgXTknOwcGT54qBwx9VCZuG/0fATd5uuLLz6XZ59+SubNnWP8AhvVupvy+huhuk2X+ykrpgdYzxeofdhBUw+Wb593vlqbT4kkuZg98+6qTfLUwtWycFuplNc1SJOWv6UzOOWX0+6K6OnaWQuyMmXq4D7y3cmj5aAhff3uSvxP6vz5599TR/7/qXW6tn8FqyYFbg37pZmLAkNMe+im7SFN+ilYHTl9ijpjf1MOOzQ5VzytgThpDUdnfvqxvPTOa8b7PkXdB5CIQlket7TQIXevSGNw7tNnA1yjWVqWylgUA16HHXiwnHPKmZKltkXxoqamRvnzPX+Sxx/9u9SqZXiaggrgYipJy91OWrzuaUiX3aSpQZ2btczdNFq0/Zb2ewN9wQJfzwOCuLrkqu3QZZdfIT+8QmMyIX4kkKrrG+WOmfPl2a/WSKN2SkCF+raVPmq5MzxQXjQ0qyO0SlxXHLyv/PjQCQnngVVAwOmqn/xO3che1zrGlSi1lReOK9pKKfwn7Z/2QBvEGv6mmy+VG2+4yLS/8E937g43hqNWqILR+srzg72WDowPYKRe6sHSDXf+vVkfyr9feaHdjy8cWHVPaZFeBZVSV6Mg49fCU1JbpEdetdTXZQTs2KZTaEOgrNCaovVSUl4q+6ukZZ0Ll9/OXKex/PbO38jDf33QNJJ2sKJx7tZAu0nO4O0ydHKJ9NunTlLSfVJflS4tTXRuB7nQmyxJy/jTqQ/ZrI8+MqB96GHTHCQQm1saVHK48a3P5WkFq3Sth7QYghUlgFVGJaAg3awd9sN120BxmTZMI4Y4YmRs+ECqTPkuvGiG/PvZNyQ9Ld24SFlt1HprU5NKnQpkZJVrgI4GMTXnuiNCKnGdc1yzzjUrQLdKaK3PcZ/VHlp9Cps16MBngoBw1FEHcDmmRFw7QkGRxxD0T722JrHDaYjcrd2wXl56+7V2Roa4tf1SS3N3ye+nITWy6rXwu3puS4ue71smuQpY9vPtD/p9ofIyVLr54su58sFnH/tdjc3PN994Xf7xxGOOrcUbG1pk68YGafBVyajDt8n4EzXWUb6WW6Utt2RNEf/20IPywcz33T4etfuf+XKlvLBUY3gZSWL3ZJt93aW+JVUau6dJg+ih34MePnVU9x+1dk+y/QzAlZGaIn+ds1w+WLOp/Xyivtx33zPyv/++ow7HWaYP+OcjNbW7+nv21ci9/dUfMtMAUE5Ohv7uZ85zHVDq2zdXfRD7q9/kLt3UsGH55lxBQU5AkKA9MJDd+ZtHNdLFF/6vTujvpNRhgbTvzfpAaupqzOjilEMAU3lJnoJWuWxYPVhHFNX76MMp+pmXXylFq4bqiLq7g26w9FO6p8gHs2fJwZMPUmV87FwuiELw+KM6wjBapjoDHF9TmrQ0i2xZ3U1qq0SGj6+TsUdvlcVvDpaGGrabaisVDLC+ByuonqeREgGA6ei0w48wDTbE7VG/VK4SxeMLVklqkCkpYDU0s1jG5WyWqrX1kpqXJml9FJS0Ou3AZBW3UQMPzK8slLqUHOnWqqkPm2eEkjqVWh6Zu0IOLxwoqW3SdtgHo3zD1q0lGr/seZXsWyOC+icPEGVnp2sggYPUFShTN3D9XDfbKNJQOAM1lMzBGvWiQWODfaCfVerfWKhS0hiNirFdnehnaZrd1OF8fxkxokCjoSzRAI1LtK6tidauN9Ee6urqNZzRU/r8gfpccsg2SQlYZboSuHLtam0wbrPnk4qSnjJm/9WSnqFjcHqjNDakGamjtjpL9UIZKlprL3dITAWLS0tk1bo1csDE2Dl+L9NIChyMak6puTZLO6169GtbK9/eikmFExtk4PhyWfe5OlsbXV03SU1X8V+L7ETyQhG/cOGXGht8jY7UY51mJSr3zdu0Q9aW7wwKWE2+FJmYq7HMCt+QNbNKJbdntvQqzBVfY6s+zp4JMK+qokFu/6ifLN//exr1TfnqELTQmS3YWiprSypkdL/A0Szs74rF9w8/nKuRJbYaRbiT9HfNGgKNTEwVd0+l9Vyg+3fdS3v89LNF2h42aQSIobsuJPBbcsCmHwO2FW+XndU7zajvdynoT5Tt2bm1su+UFVLQv1QKx2yQQYVbZbh+Fu5bJH0HFUsvlbKQwtwQphMbNm9084jre5cvXyrWRpaOHtbt/ZprNUZXjcatUmACtMp2iGxbpzHDhlUbkErPbJEhkzUm0sQiSc2p1g4bunHyXqbC1TurZKXGqIo3Ld5eapTsod7bolIWU8DGbmnSqLEnzXcFMo1kJo06BbQfDaLbshXNlX7zrfha4cvPu5kaVjU0ycriilBZiek1VgN9vlADKyvGLSYeFosmLS3EsUIv1WQWUVrjZDGTQIneeg5lOr85uM5zrfGvOBcA0fQsUlZ5WZUsW75OfyUHuRVh4pLrCkwXdLXOSSwmMoT0kKNgNWHqUsnpUaMShSpR9bCI/TsBq/30+ldzxkllWQ8zXbSuh/qkmZdXxbbxEnwvjMIxYBYbivtLum+7pGRXK2j5ZPsGkZyeTZLdu0Gy+pZITWO11GzLlKYAixABE9STKGQJ/hZv2r5Tdx52+dJUnd4vqR4kKfo5NnezAp4idxshQbSk6N6DK96X5uzeUjLxdOnWTKcNTyjgt1fXhr8xRnds0Slh4Hk80lK9As0yNXdp0Yi8i7QdE3u/Tu9v1CCGy3WTj1mmLZWXk/8WjbK6UnfnSVeAArhay/T008tVmu+uin3ATmchjTn6e0zA0rBqzhQ1WWhXr06WHGk+QH831E11VCPHrzVgVVududsqYX1duq4Opkl6ZoOMnrBGZwg6ItkGFaSuoJKX9qJ6l/lxk3furdNoo06pmfyoFMBR15ImFTsGS9XWodJQ0Vsadcq7fYMuf2c3SGlRTylXnV39tiEK4Ew1bQUO87KaGpXI4ko+1R2FkigCZwbd5ObqPvKXxadLSUMPA1y77qS82sG764awi16SXqs/VAALrBPa9cyubzUNzsBt1xPR+1ZbW98hMXRWjY3NClQcTQpI1QowzTq4tEpPWVkpGlY6S/VaKW2SU6OG/EnXc5k66LeGkQa8+M2B9Favhripqi9NSQG0atvSbn1Hx8GTSLDO22eHjMfgR1JKWKZSHHYwgKagX6n0VkU7jxSt1E5an26kKeb2SFfLFoyRnr2rZJ9x66SHfvbpXyJbN/RXqYSdUXxmBRHbrXLVfwUELoZrDgWJWBDK7nBE92vU9w9UbfI0DSU8VhWiDTq6blA9w8qMTFlbXSBlaX2kqqxRuum0qblBbdaUIayV8dmszzJx4LcKYyGpY4MNeWvULtarstsNIVWld280q4brdgySh5edLNfu919J6wbwaYnbqqy1zjRa7ZynpSmzl1QNniTdm8MPiGFY5Carru+tN2DZ2taQeFkNnDRJBx5lUVVVuZocLNI4a32FaK6YwwSiFm37zXqkKaiHIoJMpqVlaQTUycqq1pXFjz9eqRFPa4z0xrOJaA/B8pyUgLW7GnVX9g2g2FoToNQzv0qam1LUGjpVBg7faqaI/IYApMIxRaqAbzJTRVYOWUXcurG/eaZ3n3KZePASA17L5o+RLUWtQLbrjW5kE/tT0ftOcWl23yyrkAtKy6R/W+xtmjTXkLa261C6UK22V6ZnmjL30IY8UHUVvVWkT9feW6f3rM5Ilxd65clWBblUenQwCnEp2COdOm/e5+ylqQpIqd11mtKYL59tmCjvFU2W7JR6mbtxX3kq52i5eOTbypNWRXNjI/oa/aVl79ZQJwUfPyr1R/9EGvsVKqiHkehC8adThXXwsO3dSFcDB+ZpSO9RKgXpVFWn6++8Q/labav8waShUVuFXuuZ3iBD8mplRUkP0U3bUM5JeirPdHw/z+fmZugOUfsYnRUAOX9+kU45kbJbQTPxPWBXnpMSsHZlr+M3bXfSd2Cx2lnpRgSqt+IoK9ZRszxXTRaGmMowLNY/vi0Ws5XtrJiZ/9pw9QtTxJSUZsnsUWfAjnoB+MxUseMr4/LLvxH5vxTp6AfFpXJJSak2vlaAst/DvB5gGqaK1cE6TShUV5Z8BbUWfY72yQE3pu+slgPUfODnQwZKpSpUk0cfQA7DE1PA9ToFfKbvybKoeLxUb++p0qJ6Juh5pMg3Vx0sg7JL5KRB8zRWenddvu+rgxSApWnrH6y3Nme9J/+Ui0wnZjAzzAn/6oTdAdiWl9eofmqbSlM6C1BjZn+QInMNqitoUsHx+wetl1H9qqVHZrPs07dWFmzooUa4LfLSon7y7rr+kqOLMSmUu43gTV1do5o9bDOAhcTGb96bjNRlAMso1tXwc5yuAjKVg5CWNq8bqL+bpZ+uAlrTufZlXtMelfFt9WOmiApeZcV5phGP2X+VSYe0cW1pf86cTY4/Ddpwvla1Uy5UyYop4a6mtit/KdqQF+gyf+PtB8vgQZkyZ8Z8OearLdKEYZESwMRzpDWxtk7OVknt4T75khEOKXk4SSgtpUkWlY6QeavGS0ltLwWfJknvpnqmtn4F+DSpbdqqikHiGzhPDS67qz9cR8dupLOV1eoNsEIBrlGnzPp4c45pJElSyt2zgQHo0qVbZcmSLXqRqW6tmQbaAaVJwWpSnzI5bkyxHDW2TNJUkmrWNs35AwsrtZ+IDO5dL2P6Vsvry/vLjgbdyagNtFgJLC7eKU888Wn7y0nbsopvP5kkX7oOYGnLTGmzoWIV0IyOWhEGhPwGA3MNBut5/y4OyHGkZ9YbO62WZt2CSm/lnAV4SVI3Jl+ZCirfUoBJ0U8krYCk1yoLsmT6frkypF+GrCvsIQ0KWAaotHClSI/6YA8Fa2YMx+i2a8/k9zLTxCApBnxNwk5qJtNqRHaU99WpnDotA1R+1KLmDSMHFMuF42ZJhhpCNpvS225SPjTp9NDS7bdop06vUABQn8wWdW0KOBLYHk/0VztA2fPC0O1T/d/lhxfJqAG1Ul2fogOymn00UPutlJ6lezDq6vGF09S2S13U7v1klPTMUVmdht9GdoBK5nGsywAWfLWACAV58bbesm1TX6ks7SmjJ66x+O7ok36/Y3NfXSVR8TiVbbgVDBS4Srf3Duoc7SjhKN/ElG6orubso0p5pKtgRKNNLciUvFytTm1t2YOzdfKjAKWS15z8XPnqzNHSsqNOTnl9hfTTqWN/Vdyj31qVoYa0ydw6KbAWO6VG9S9l6CS1h+lvdFQYkpL1tO6t26jXNqhb0qJ/yAsrl6k9VkcDXO5jReysk3pL9/Q2PmobUtMtSanVXaABrCQl/AX333+w6rC0DrU+y8pK1LL9Sy07edaZgU55v3vABhmopizVdSlSWZIumQpGNZXdZcPSHBl1gLpB6IBVviNdfH0b5MjR5fLVlm3yibZ/JDGmgH369NAt16bpoI05kE+3gftS93hEMtsFesnCni4FWBiHoo9C0sKWaqsqyHFqduNuA+ORurB6X7N0eId6IG0LFDtcSNAP9FV9VCeVpY0TUIKCwZa2ZeWN3qDTQF/bVJAeva1Pjpx5/lBZvWKnbH57rQxQESNNz2drQ03ebmqKav5006XNtIrWBRSk6UZJkWzZKfv1UYPgPpvknbUHSXVjgUzp+Z6krVsk6wyw7Vp1hV/07bQ0BTlVWnezWzYoU7s3GQTc9cIk+wYw9eyZpXtE9m1TutNGW1sBq4ADsmvlrCnbVI8nOninS1WJTpZVjM7OU3u8nqqnzW2S0q26AaqWMzVDwSm/Uc6dslVmbyxQxrSupmZmpuq+jn3bAIsdznUtOUkbR5cBLKZsOytzZcXCkWYqt12lq1S1RTEUrBcHaXyM0NQ59lvJTqzwdaP1aIYpZo0WGemJ9sTvDP3TQ69lF1XK2+8Wq0tNrpR8vE0y9Vwjzw3L1Y6dpvqabJnXWx1pN2NTw5NdgDSbqTU6/W+r5hSt76P3mS9T5r4h+w7TleERA+WdDdPloIEr5fpJCyTntDFG+rJKZqQQBfxu6poDv2rVx66smm/KAe3AKfUqXamUkczEVG3LlnKZOXOFARHMGihXK2i1rvrVNWpQRh3SCDVUvi1dbfM0ttXwOsnu1Wgkrs0rsyW3d5Pk9Goytlt1ja0qFZ/OLEgHw9OPPlqp3wEw7Wc769vSTz7OdBnAgnUox7esH2AaH9NCpCGjwzJnnDOXftwVCFipxTZGGxXCU6Xi65snj5acEbnSS40ES6ubZftX5XLux2vlwO3l8uH8EtnWI1X2/WqH4EOtKhuVQFuBrrs6vRoJTNNhelmj4n7Sw5bmv3tday7rNJLCkft8JVeMeU3WzynR1b9sKavRzrl5ifQse0FeWF+himaVxKxCacfL7KHuS4MmSePKz41R6XGH91RJq3WQStMpkwFCu8SVhI0Cp+M1a4qN83Jr4bBqx35Q61ElqeP3K5beKkVV7UzVutbdy48ok7rqVNm0PFsqtmdI3+G1Mv7wMtXZ+qSqNE3qFawmDqmW/fpVyvwd+SpVdZOKihrdU3GhAUTSxRnartNKJrZ0KcCCcdYKocVEQAsmuyIVpbsCAVLF2lEBlzw1U1iZ31MO+G6hHDQuR6VL9Z/TEDOvfJwnX365TaZVVqlOwpIcW6dBVG59uVq9VzVJcYXOrfR+0izXTlCijuXdkxy5AZRumPZrpht6KQBlEBZYDSJ1WujrliIZ9SUybvNDskIlhOUgVVu18oF7zYqCUySvcj8ZtOAttdVqlCOnZsvGRp36aMdFckPCaEZ/xQNJOohRRZmZaeYgo83KD42cbcCFYBKLt+RKTb2aqOgAjspk0/IcjczaOiVsVikSn9ItK3MUvJt1itikAQV8srEsQzZWqPO8PgPh5N+7t/qltjGwpqbe6LLMxST70+UAKyD/tOG5IYU4N7cn7F4AZYcahG7RI1+V5N0KMtRrXsFKpSWpU11UZorsv18P+aKHigmVrdkkKAGglKZKra1azsraFtlU3iwLHlklR6siFR3XGlW2lysQsvKY1KTFpGqbVTpo0SICVhY1a/n6a3iZO28Ypqd2ned6mtorvLTxENk8/xQZ32+5XHPaRFmp5g7/2jhO5m4eLSkqZZiE9bHmLH1BErMBd5wDDhgmJ5443ijdi4uL5eGH5ygvCMinnh0KPJgvZCA5ajmqK1slrQGFtUbi6j2gXlbOyTMAl9tbXcZV51tekyrFtRmSlsFiU4vGzMqTSy+drumhdG+Rf/1rtpmGJktIGavO+dwjAKtVUd6x0doLaUlg7f2TBsoR/BH74wn7TvaqtREtyM6S/dV+Kru4Vl789RJJH5ojR5wyUN57ZrU0lzXISNU5WH2ODl6vje6j3j1k53dGy/FnDJH5L2+Sie+sk156kftm52QbN51WVXbCihf2xaoTFp9KBK2reLtXFitaO8p0YwqrYrWiNWaDrG4cIU8umi4pakm5rqK/XPfhRaq7anW7wnXHtBdlRGMPXXhI0y8wJYkpPb3VV5Dy1tfvCtlNOerUo2NLebqM6F8nWSpB9R1aJ70HqlS9KUNWz+spk44tkbGHVEilmjogYbUouG0o6xiRF2Dq2TPTxLxiVTIZgcqqnj0DsNpEW6tQ/p/1KmU0aWVnqvUzoVgg2ujuXcBciv+fEBlBWpqpuphv6M45+5ZXy5gXl8q7usy99ej+0ue51XKE7iDUrIVq0NKQDHZW808ZLcd8fx91VfLJ7Du/kikfFMlQnUg1q3RVouL/JwpY+BQmPWlZmlUKsJTu9vwSiG7ztga58daVQrC+VhbqlDc7V9KOvVSBKFMlhiZdDc6SGr1KZAexFlm06E252iYUsJKdDYDH6tU75NlnkarU4b2OEEnosNBrqv1dc4bMeGOM3Hv2Esnv0WqfBjix4r3P5CppUFOHJpUo8/o1SE5Wk3y8opfcP2uESufKTeUDuqqysmrd6GKeSRPsL9d2Fm8dVvugY6/kAN8dARaJOU0wwDtiegpFPCYOeb0rleGscrQ2XSqMeX1ttcazWlNn5uS9dAoxeHia1OzM1jxxX3J02tYcB2YTJghf6UYYcxRkjlDXGraeSKlv1o0jUmX5tIGy9Z01MkjvYSG/QbXshx3US4brtHHBS5sl6+mVcmJppToJ65K+NnCMUN/ukSub1Jcwuazcg3AAYMlpkbRKjaCKLstG2CT175suv71llNa51qZe1lPy+LrTZGmldkiVtCCj42yrZwyDjfFtXqMBLFtySfsV4Ni6tVI2bVLFlWmzKN13tVv0UKUNmfLesnw5YXyJ5Gar76iaL2A46tPQMYQdSlU9HdzbXJoh767oo/Znys82QxmAD53VF1+s0ztaic0u4g1Y1rvDfToCrHCJJPI6DXLdimHG6HPYqI2Sp5tQQFs39JOK0l7KePW9krX6qZ7uqmxcWjpKdmwp0N86unYRopP9Uy3TD6ip1bB0LTJxVYnMfmCFTLl2nCwa3VM2/mOZ7KcRHIh0u2ZTncy5fq4csWqH9NWSN+kIjVUABqKbVRf2XO+8LqUHwAa0MU9LwPzQRoBURnp3GTWCwacVmGqbNYzQDnXirdzVoenk3XSBwacNID+jWCYOWiXv1YzXzkynTX5CUOjdO1v3DST+upqqNO6Udes65jxNp7VPzB8uLy/uL7ectEqGF9SZII5ZGm22qlZ1ldoOXvuqQB6ZPVwHrlSNXb+r7cNHtvgqLOzfLmFt2VKhU08McuPDHzfCkAVY9hreLZckGCyMxW43x/uEMhVXneJtBcafsFef1mB7hEretHaQztt9GoF0rTZOlUI0kF3xNvU9VG//LtFa23iJlLVANz99sncvuUwdoPurw++RLy6XWUU7ZeTPJkjLIQXy+h+W6N26+qd2VpNWFUtfHXkt63jT1bX1Pdy3QDYmnXQVpsFoy1SjdgWYjvex8LCmqFYefnyTuUDf6q6auaJuf5O68T+RHA25gyRS11gvOzctle7b58qvf7hduuf0UCljgl7jiZDNXq8nntApjRs3QL7+9UlmlrBjxw65994PO8x4KAlRoEubs+T2t0ZLD433NqBnnRw4tELeXMZei91kY1WW2pyl6tS4Y5lbLd17ykUXHab9olXp/re/fWwkunjpssiDA9AyGe/6gNXWppCYjDK17beZEqpTdKsFk1aZKkLwNzOSFTXcxQjQ+kdBb7V8bzb6rAJFoVPmb5TPryyXmkvGyaF3TdGtvpqkcqsahqpUhQGgacgMoUqPqLPz6z17JNlUsK0STH24qxSKlau2aJMn5LYl0vpxUMoGKRvyrryz/evaOZvkzH7PS4+MeZIytEkG5eRLUcuu3WM6PJjEPygrnZppsCU4GB0W0wYbqcApVS1qf6WzxqKanvLp5v7G/YZbsNFq7eyB+czqIO/hM94UCWCFzCXo1xr/OT5FYbeaSEQgo69gBNH/iLPdVNHa0NBb/agm62p+nUYO1dEmcH3Fp2BB3pLiYLMNsk0l/bFfH1mvIvxZqoQv0CXvg3Ub9033zJN3Px0qx980QSpU50HoGEKCI2EV6b3PqmT2al7PpPYbTNF8uyHMGvr1SZfvnjOgw2Ok0tT8pTQuHSQfb5woJ06skCE5fYzvYYuahvhqtHEkOaWqjskiojUQqWHTJg1QqdTQUG1ApUY32kXaciCZWEkF/GzRuGBEa/j732dpf9F5iLKnpGRnXFcKHWKLwShLwgoLWASujxexvZK75tuas0qNi9Vb9x9sVleF6vIsyRiqGp+qRqmtGCAZI9TqeQNByUIWNWARUwFQEDBGlKF2UU6I8ZTu9rQC0BsqLfVRY1I8S5jubt6gQe1e2ibTD+gptw0cIMN02ovB6RadAlbpJxKa0xKkKcjFm9KxLXNJSBx16h/oT2wkccHI9zQahZZDJRMiDreoxIn1v1NiE9dEUYYGWrSmq4DIzp11UlnZ6lJFeBmoTK1HOaJDahnfBoikx9TQ3tzd7OYUSX4cYksHwEIvG5KchPENmYCLi1mqr+mmTHNDOEBv3TZESiv7qYNrqtSok3pWf10L0c/uukqSOVTDt613D1YgRLbmJ5bUU6Ufp0Sfw78QEKqwAV2tilQ9c1MkN7O7lKpldIl2VqQWjEO53ynRQfLyejm9PUr36TujCJLG0lujkF66z1smZDK/3RCdNc+AhpunondvrzysznfVGXWCGUcroZsbo9vJp6kx6QQNVJguhDRes2aHFBb2kenTR6vpQ5O8+eZidbmp1X0JR8iECQPVELRC3n13mYJRNznuuHHSr18P3dJto0YX3aDGpOkGpNpe0PFDV9579+o47e54Q+d/sS+nAzIYZUlYYcWnnRpDKV6U36u3Wu7qwrRKEFSWY9JbW9QOJ32gWvE2agAltc/xaZhcNidtUctws+7tcuqBWNInX/f5iyENGzZcG41VFc5eBJxbrjXYmH3/tL5y5bkDJFsB689XD5NbHtqoLhvo9ZylZ+4C3HRvwiFDhrp4KDq3DtdOES6r3XW6j+Fnqk8lS61cvuNeE4x6pdaYKY6lBzDhlQMZddkSILUMHQyG5rWuPtouxe3ryJFD9F3BuIEbToG66mQpEO1nIjksXkyHb9HVxKEa+32ScV7+4INiBawqrct9ZMqUsQpw23QHnWIDfGPGjJMRI/roVDBNFiwgdjtT0N0Hc6abWZnpMlw3lY0lVesKdxiiWgxGWb3EHgAg4LMVFbHd6sr+0n4FfaWvHhu3bFIG75rP2+/x/86uyc2qdNZVW2nSEAYpObriUaM7jBBSRAGLa6w0MX1C4DBAGEaIo8IydbVpxNBh/q+L6u9xEyZI3379pFh1Ek7La2UAXc6AgnT58Tn9pacqoX0KXidO7y0z51XJc++XSCaaWIfE1mpDtKxjxsZ3E1WyN2VAvko1aiOnq2KBxhTAqqSxhyyoGi5bcgokS9IlV1e+0NU5pRQFq031BTbZZfcn4edw3U15TEHHaKW73xm7M9Om7a8AozHNdGuuwPZQgAsH/dj6jtLc+m4/b//OdYCw9b5d9wcGR9yCRo8eKuP2HaHPxI7KdePkMESGOwAWdocqj6i9XRCK5151Odk5cuCESVK0eYPakIQBLOpDKWusOn2qRXvjjgap31Ar3XXro+ZKBbC21t+oVtEpPdUgTs+n9FDr39JGacYhOHBdmTSbNDTJ+FFjZeggRrzY0ZDBQ+Rrxx0v/3ricRXx3U0/Ad90jfXEARLDDmzec9Sqn2tuqEn1lCeedLJOF/q5eSwq947p11uOHJwvL6/R0Dg2pbOVeJqu+M2pHClfVI7SkDltZ1dZV918turyGLgCUaPqvE7dZ4D0UR1homjKlH3l6GOmyKuvaPRU27Tfyg9Sc0NDs5nSZWWlqS4LKUlVBBp1Ye7cImNDxXXObdxYJnPmrDPGpzxPm1i2bKtRrDNNDAyIrW9iY9Zvfus4yVcbwFiSA2wBm8CodhtCZEpOBgWsoqIi7o8PaYUcesBUWbRiidkmPpjSr5tGLEhRi+/UfLXcHq5ze62M3APypGFzndQu3Sm9jutjpodIHU3lutvtZi2zpp01MkeqPi+Xlp06pdARNRA16+pJT7XZOe6IowM2mkDPRHqOhnXhRZfIZ5/MkrVr1phpmdO00G1sVDD+3wdlcp5uuqBJyYpV1fLenAoFMefSFTrKCRP3l++cf4HTV0f1vtT0DLn8wDEyZ2uZbFcdTFogMUvfGAxoopGZOl3Sn1SQKxdMVlCEkQkiQOqmmy6Rz2cvUWCp1HAvOm2wEbOD2tpGefHFL81Z2gAhYVCc48IDsbrIZqkLFmyQefPWmxkFdlXMGt55Z6n5pN0Fs7WiPRxy6ET54WXfMOnF8o8DbEG6MoouS3xhwn6ZHmj0AlLv3r3lggvi15gz1R1lUN8BUrRJ7Woqys1osZs+C4lCQ6Y06V589atrpGFDnTTtqFfwUqW9ToWaq3SlbFGV1CzZKXVrdLPIEt3lVjs3YNZSq3MJphN+eEWFMjXKzcqRs086XfafMNGdHi0g98KfhL/7jBol8+bOkRL1yKcx7VbeAMko/hr65KudsmhltXw8v0oeeH6bbFMJ0kR1CPCM/RQ2MPWq9Byt08Dbfn2nBgAcY78c1+/9e+bIENXBfbZR9S+6cSiLBlb5YpkRxizAamxeltx1zP4yWncVCil6xzIzbWkPHTpQp+d95P2ZczXAXo0BFv/2gHTEYZ3n0zpnZbH1HHqvXZxsvSdw+2LltUHDTU+cNFr+/sjNGh2k0EoqZp/33HOPrF27NlT6unQmf9Zjp1WKfP2xSI9BegQkNm1cuHChKvmcr2gFTMjNSQWPTZs3y9sfvScLVyyVmrpWJSoicaCmbLCH1kep+MHMl4HSlNIqqv5uI9KBzHOAn35PV2V/4WC1aTriGJkwdpwa3HUc3cwDMfzz1VdfycN/fUBmffShNlTqSbNvZTTIe63i4ksI4Kbp6MqqvClXkGe4D8rLy5Ojjvma/OCHV8jYBOiudsueTsM/X7Ve7p+zQmZvr9QIq21TuDYe7F6Lu6UQ9oTFF4sHPZRfXxuSLz8+eF8ZO0S7QAKlq46Z96mifLbcccdjMmvWIp3qMSuCA4BUxzs786u1KcAVwjHnyhlnHCm33HKJDl4jO5Oso2cxzZg4caJKh5tC3V+kF/fXo8IqNoZA8/QYr0dAQhk8c+ZMDYZ/RMDrsTxZq1u5rytaLyvXrZatO7ZLtW6lzgoiDG7rdxG93qp0ypaVkSV9eufLPsP+v72zCYnjDOP4Q8pKqjkIoWVJFfJBwKaGIqyxCDkYggmmt3ortPSmSEHoLXfxUvQmxIOl9ag04MWgtIJtUGJCS4JVW5u00aZaGkPNqYaY/P+zO8PsujM7uxNXMf8HHmfn/ZqZ37v7+H487zvHsT7tlPNDLmQoSrpohEycNbl3767dnpuzPx7ct/8wKPn/FudF4gsrvAJdjurqajtx8pQ1njtn79Wf3XXXjaLuHJW6iUmeOw//tluPHtsDjM1s4m3IWzBe4WY44lVQ8ZwJrMYg/2lsXPdBDd6sXJu0N4/s3bhV2J1zUPrHmz/ZD9M/2/Lvf2E3hadY3Izvv/vrDcscIe4wZgKPYpLhzJnj1tKSssZUPYZBAjtbEUqMnmRqagpuFhc9D/6AnHcRnoJyMxJPvsenFu8sz4fu7m7r7+/PE1OeoG101bbwxX2GF4ZucykB2xD8DscREGD3K4GWVCJRkXYv8FOJU3bMvGwB0KmO+hzP7jxqTAvNR6OB5rggn9c12jFvdXey41lfoK7TdY79yNF9jV3nAMB/RG+gzitQ53TjQKXj/vdJpYeSRPcd/7ie4TfgfR9C04dHOk8MFnTU5gLoRCKaA3N4qcXFdnV12cDAQKFME0hwiYn8tTSE888YGCTJZBKzEHfs2LHAnmNQVoWLgAiIQBYBDranUilniVFWxM6TawjqYLB/KmR+Z7rskLW1Nevt7c0O1JkIiIAIlECgp6cnirFiyZ5t8hssjmEV7GANDg7a+Ph4CbenLCIgAiKQJjA2NmZDQ+zUFRROndE2OeLvEnKmkINb76Sjgv/W1tbaxMSE1dXVBSdSjAiIgAjkITA/P2+tra32CB4AEcSbIWRafwtrA+dTEQqwlZUVa29vt+Xl5SjJlUYEREAEHAJLS0uO7YhorJjnO6i3LtBvsBj5DbRgt5AJaSXb2trgH3KTpxIREAERCCUwPT3t2IzFxcXQdL5IdgdpkzxxPd3dgIf4cB56wg0IO25sbNjo6KgzRd7Q0OAcw9IrTgRE4PUjQL/Cvr4+6+josPX19WIATCIxZ/m8RlSuwaJFuw/9GJobh6CdQu/byclJZ0yLy2lqamqssnLvtubYeYcKEQER2AsCfOnryMiIdXZ22vDwMPzpinJ+5tpBulmxEeWJf9DdC8SHL6Ff+AOifuaAfHNzszU1NWHz/HcdA8Z1clVVVY6THvd9ouNi1LVyUa+rdCIgAuUhQIdmrkGl8yq3N2ajha0oLrNZXV21hYUFm52dtZmZGee8xLtiy+pqbt4gg8UtBum7EHsdDj2JaayobHlxJTrDXMNFr2NXc29O5yIgAvuDAI2Uq66hYouJxor7y9NgUYtsRQU9HCf/PoRiF85sCTJYTHUSegN6micSERABESgDAY7IX4b+me9ah/IFZsI4ltUO5VEiAiIgArtN4Ddc4CNoXmPFi4cZLMbTkfQK1PM0ZaBEBERABF4xgTmUR1vzS1i5UWYC/0UB30K5q9n7YYUpTgREQARKIPAV8nwCDd0Ui+VGMVhMx8Gv61C6ttdDj0IlIiACIhCHAMerPof2QNMvXCxQWtige1BWGqtPM3oWx1LKCCpb4SIgAgebAJ1AuRn91xl9UszjxjE2fL1LI/QCtAnK2cS3oftz20bcmEQERKDsBJ7iiv9Af4XOQumywPEqOoYWLXEMVu7FaKjegiYzR7bE+H4g+nTR9f0wNAHl9o7sinLAn+reg3tEkEQERGCfEWDLiMLjdkb5Ghfs1Wx0YeeG8xw6ooHiYuXHUBoqrsXhsXxvYsbFJCIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAgeFwEtxj+eHnNHaxQAAAABJRU5ErkJggg==" @@ -732,18 +734,21 @@ class BatchError(Exception): def run_batch(input_dir, output_dir, leapp, *, python=None, type="auto", jobs=1, timeout=None, heartbeat=30, skip_existing=False, dry_run=False, capture=None, - hashes=True, extra_args=None, log=print, should_stop=None): + hashes=True, extra_args=None, coverage=False, + log=print, should_stop=None): """Core engine shared by the CLI and the GUI. Runs the chosen LEAPP tool over every archive under input_dir and writes a master index plus a manifest (CSV+JSON). `log(str)` receives every output line; `should_stop()` (if given) is polled so callers can cancel. Returns a result dict with keys: ok, failed, invalid, skipped, index, manifest, start, - end, elapsed, total, tool. + end, elapsed, total, tool, coverage_db. `type` "auto" picks -t per file from its extension (zip/tar/gz); any other value forces that -t for every archive. `hashes` records SHA-256 of each input. `extra_args` (list) is appended to every LEAPP command line. + `coverage` enables the developer App Inventory artifacts on each run and + aggregates every report into batch_apps.sqlite afterwards. Raises BatchError for bad inputs. `capture` defaults to (jobs > 1). """ input_dir = Path(input_dir).expanduser().resolve() @@ -772,11 +777,17 @@ def run_batch(input_dir, output_dir, leapp, *, python=None, archives = find_archives(input_dir, exclude=[output_dir]) result = {"ok": [], "failed": [], "invalid": [], "skipped": [], "index": None, "manifest": None, "start": None, "end": None, - "elapsed": 0.0, "total": len(archives), "tool": tool} + "elapsed": 0.0, "total": len(archives), "tool": tool, + "coverage_db": None} if not archives: log(f"No archives (.zip/.tar/.gz) found under {input_dir}") return result + coverage_cleanup = None + if coverage and not dry_run: + cov_args, coverage_cleanup = batch_coverage.enable_coverage(leapp, tool, log) + extra += cov_args + start_dt = datetime.now() start_perf = time.monotonic() result["start"] = start_dt @@ -939,6 +950,8 @@ def heartbeat_loop(): stop_beat.set() if beat: beat.join(timeout=1) + if coverage_cleanup: + coverage_cleanup() end_dt = datetime.now() elapsed = time.monotonic() - start_perf @@ -958,6 +971,10 @@ def heartbeat_loop(): result["manifest"] = csv_path log(f"Wrote manifest: {csv_path}") + if coverage and not dry_run: + log("") + result["coverage_db"] = batch_coverage.aggregate(output_dir, log=log) + inv = f", {len(invalid)} invalid" if invalid else "" log("=" * 60) log(f"Done. {len(succeeded)} ok, {len(failed)} failed{inv}, " @@ -1008,6 +1025,11 @@ def build_arg_parser(): help="Skip a zip if its output dir already exists and is non-empty") parser.add_argument("--dry-run", action="store_true", help="List what would run without invoking the LEAPP tool") + parser.add_argument("--coverage", action="store_true", + help="Developer mode: enable the LEAPP App Inventory " + "artifacts on each run (iLEAPP/ALEAPP source " + "checkouts) and aggregate every report into " + "batch_apps.sqlite for parsing-coverage analysis") return parser @@ -1025,7 +1047,7 @@ def main(): python=args.python, type=args.type, jobs=args.jobs, timeout=args.timeout, heartbeat=args.heartbeat, skip_existing=args.skip_existing, dry_run=args.dry_run, - hashes=args.hashes, extra_args=extra, + hashes=args.hashes, extra_args=extra, coverage=args.coverage, ) except BatchError as ex: print(f"Error: {ex}", file=sys.stderr)