From add7d642fb374536ab7d1f4e5849598cf74eea9a Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Aug 2026 12:41:44 +0300 Subject: [PATCH 1/7] feat: dependency scan & update --- .github/dependabot.yml | 25 + .github/scripts/dependency-report.init.gradle | 34 ++ .github/scripts/osv_scan.py | 170 +++++++ .github/workflows/dependency-security.yml | 86 ++++ .gitignore | 12 +- CHANGELOG.md | 467 +++++++++--------- app-java/build.gradle | 2 +- app-javafx/build.gradle | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 286 ++++++----- gradlew.bat | 41 +- sdk-java/build.gradle | 2 +- 13 files changed, 761 insertions(+), 372 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/scripts/dependency-report.init.gradle create mode 100755 .github/scripts/osv_scan.py create mode 100644 .github/workflows/dependency-security.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..bce2dc92 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + # Gradle dependencies. org.json is declared separately in sdk-java, app-java + # and app-javafx; Dependabot opens one PR per module, so check that a bump + # lands everywhere before closing the others. + - package-ecosystem: gradle + directory: "/" + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - dependencies + commit-message: + prefix: "chore(deps)" + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + labels: + - dependencies + - ci + commit-message: + prefix: "chore(ci)" diff --git a/.github/scripts/dependency-report.init.gradle b/.github/scripts/dependency-report.init.gradle new file mode 100644 index 00000000..07247450 --- /dev/null +++ b/.github/scripts/dependency-report.init.gradle @@ -0,0 +1,34 @@ +// Adds a read-only task that prints every resolved external dependency +// coordinate, for every module, as "group:artifact:version". +// +// Applied via `--init-script` so no build file in the repo has to change. +// Used by .github/workflows/dependency-security.yml. +gradle.projectsLoaded { + gradle.rootProject.allprojects { p -> + p.tasks.register('printResolvedDependencies') { + doLast { + def seen = new TreeSet() + p.configurations.each { cfg -> + if (!cfg.canBeResolved) { + return + } + // Skip configurations that pull in the Gradle/plugin classpath + // rather than the shipped or test dependencies. + if (cfg.name.toLowerCase().contains('classpath') && !cfg.name.endsWith('Classpath')) { + return + } + try { + cfg.resolvedConfiguration.lenientConfiguration + .getArtifacts({ true }).each { art -> + def id = art.moduleVersion.id + seen << "${id.group}:${id.name}:${id.version}".toString() + } + } catch (Exception ignored) { + // An unresolvable configuration is not a scan failure. + } + } + seen.each { println "COORD ${p.path} ${it}" } + } + } + } +} diff --git a/.github/scripts/osv_scan.py b/.github/scripts/osv_scan.py new file mode 100755 index 00000000..6a27b62b --- /dev/null +++ b/.github/scripts/osv_scan.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Check every resolved Gradle dependency against the OSV vulnerability database. + +Reads "COORD ::" lines (produced by +dependency-report.init.gradle) on stdin and queries https://osv.dev. + +Exits 1 if anything vulnerable is found, so CI fails the job. + +Why this exists rather than a stock scanner: this project has no Gradle +lockfile, so lockfile-based scanners see nothing. It also declares org.json +separately in sdk-java, app-java and app-javafx -- a past upgrade bumped only +sdk-java and left the demo modules on a version with a known CVE. Scanning the +*resolved* graph of every module is what catches that. +""" +import json +import os +import re +import sys +import urllib.error +import urllib.request + +OSV_BATCH = "https://api.osv.dev/v1/querybatch" +OSV_VULN = "https://api.osv.dev/v1/vulns/" +# Local project artifacts have no upstream version to check. +SKIP_VERSIONS = {"unspecified", ""} + + +def read_coords(stream): + """-> {(group:artifact:version): sorted list of module paths}""" + coords = {} + for line in stream: + parts = line.split() + if len(parts) != 3 or parts[0] != "COORD": + continue + _, project, ga_v = parts + if ga_v.count(":") != 2: + continue + version = ga_v.rsplit(":", 1)[1] + if version in SKIP_VERSIONS: + continue + coords.setdefault(ga_v, set()).add(project) + return {k: sorted(v) for k, v in sorted(coords.items())} + + +def post_json(url, payload): + req = urllib.request.Request( + url, + data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=60) as resp: + return json.load(resp) + + +def get_json(url): + with urllib.request.urlopen(url, timeout=60) as resp: + return json.load(resp) + + +def query_osv(coords): + """-> {coord: [vuln id, ...]} for coords with at least one vulnerability.""" + keys = list(coords) + queries = [] + for ga_v in keys: + group, artifact, version = ga_v.rsplit(":", 2) + queries.append( + { + "package": {"ecosystem": "Maven", "name": f"{group}:{artifact}"}, + "version": version, + } + ) + results = post_json(OSV_BATCH, {"queries": queries})["results"] + hits = {} + for ga_v, result in zip(keys, results): + ids = [v["id"] for v in result.get("vulns", [])] + if ids: + hits[ga_v] = ids + return hits + + +def describe(vuln_id): + """-> (summary, severity, fixed_versions) - best effort.""" + try: + data = get_json(OSV_VULN + vuln_id) + except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError): + return "(details unavailable)", "", [] + summary = data.get("summary") or data.get("details", "")[:160] or "(no summary)" + severity = "" + for sev in data.get("severity") or []: + score = sev.get("score", "") + match = re.search(r"CVSS:[\d.]+/(\S+)", score) + severity = match.group(1) if match else score + break + fixed = [] + for affected in data.get("affected", []): + for rng in affected.get("ranges", []): + for event in rng.get("events", []): + fix = event.get("fixed") + # Git commit ranges are not actionable version numbers. + if fix and not re.fullmatch(r"[0-9a-f]{40}", fix): + fixed.append(fix) + aliases = [a for a in data.get("aliases", []) if a.startswith("CVE-")] + if aliases: + summary = f"[{', '.join(aliases)}] {summary}" + return summary, severity, sorted(set(fixed)) + + +def main(): + coords = read_coords(sys.stdin) + if not coords: + print("::error::no dependency coordinates received - the Gradle report step failed") + return 1 + + print(f"Scanned {len(coords)} resolved dependencies across all modules.\n") + hits = query_osv(coords) + + summary_lines = ["# Dependency security scan", ""] + if not hits: + print("No known vulnerabilities.") + for ga_v, modules in coords.items(): + print(f" ok {ga_v} ({', '.join(modules)})") + summary_lines += [ + f"No known vulnerabilities in {len(coords)} resolved dependencies.", + ] + write_summary(summary_lines) + return 0 + + summary_lines += [ + f"**{len(hits)} vulnerable dependency(ies)** out of {len(coords)} resolved.", + "", + "| Dependency | Modules | Advisory | Severity | Fixed in |", + "| --- | --- | --- | --- | --- |", + ] + for ga_v, vuln_ids in hits.items(): + modules = ", ".join(coords[ga_v]) + print(f"VULNERABLE {ga_v} (declared in: {modules})") + for vuln_id in vuln_ids: + summary, severity, fixed = describe(vuln_id) + fixed_text = ", ".join(fixed) if fixed else "unknown" + print(f" {vuln_id} {severity}") + print(f" {summary}") + print(f" fixed in: {fixed_text}") + print(f" https://osv.dev/vulnerability/{vuln_id}") + # A GitHub annotation so the failure is visible on the PR itself. + print(f"::error title={ga_v} {vuln_id}::{summary} (fixed in {fixed_text})") + summary_lines.append( + f"| `{ga_v}` | {modules} | [{vuln_id}](https://osv.dev/vulnerability/{vuln_id})" + f" | {severity or 'n/a'} | {fixed_text} |" + ) + print() + + summary_lines += [ + "", + "Upgrade the dependency in **every** module that declares it - this project " + "declares `org.json` in `sdk-java`, `app-java` and `app-javafx` separately.", + ] + write_summary(summary_lines) + return 1 + + +def write_summary(lines): + path = os.environ.get("GITHUB_STEP_SUMMARY") + if not path: + return + with open(path, "a", encoding="utf-8") as handle: + handle.write("\n".join(lines) + "\n") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/dependency-security.yml b/.github/workflows/dependency-security.yml new file mode 100644 index 00000000..ec5ebec4 --- /dev/null +++ b/.github/workflows/dependency-security.yml @@ -0,0 +1,86 @@ +name: Dependency Security Scan + +# Runs on pushes/PRs so a bad upgrade is caught immediately, and on a schedule +# because a dependency can become vulnerable without this repo changing at all. +on: + push: + branches: + - master + - staging + pull_request: + branches: + - master + - staging + schedule: + # Mondays 06:00 UTC + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + osv-scan: + name: OSV scan (all modules) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # NOTE: JDK 17, not the JDK 8 used by gradle.yml. settings.gradle only + # includes :app-javafx on Java 11+, so a JDK 8 run would silently skip + # that module's dependencies -- exactly the blind spot that let a known + # org.json CVE sit in the demo modules after sdk-java had been fixed. + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Resolve dependencies for every module + run: | + ./gradlew -q --init-script .github/scripts/dependency-report.init.gradle \ + printResolvedDependencies | tee resolved-dependencies.txt + grep -c '^COORD' resolved-dependencies.txt + + - name: Check resolved dependencies against OSV + run: python3 .github/scripts/osv_scan.py < resolved-dependencies.txt + + - name: Upload dependency list + if: always() + uses: actions/upload-artifact@v4 + with: + name: resolved-dependencies + path: resolved-dependencies.txt + + dependency-submission: + name: Submit dependency graph + # Only from the default branch: this writes the graph GitHub uses for + # Dependabot alerts, and PR runs must not overwrite it. + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + - name: Submit resolved dependency graph to GitHub + uses: gradle/actions/dependency-submission@v4 + + dependency-review: + name: Review dependency changes + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Fail the PR on newly introduced vulnerable dependencies + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: low + comment-summary-in-pr: on-failure diff --git a/.gitignore b/.gitignore index da46f720..c9858cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,12 @@ /.idea/workspace.xml /.idea/libraries .DS_Store -/build .idea/caches/ -sdkJava/build/ -core/build/ -sdk-java/build/ *.attach_pid* -app-java/out/ data -sdk-java/out/ /.vscode/ -*.class \ No newline at end of file +*.class + +# Gradle build output (all modules, current and future) +build/ +out/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df6f63c..7a12ff64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,232 +1,235 @@ -## 24.1.6 -* Fixed a bug where the request queue would stall after sending the first request, preventing subsequent persisted requests from being sent. - -## 24.1.5 -* Fixed a bug where a non-JSON server response would cause a permanent networking deadlock, preventing all subsequent requests from being sent. -* Fixed a bug where a NullPointerException in SDKCore.recover() would permanently block SDK initialization when a crash file from a previous session existed on disk. - -## 24.1.4 -* ! Minor breaking change ! User properties will now be automatically saved under the following conditions: - * When an event is recorded - * During an internal timer tick - * Upon flushing the event queue - * When a session call made -* Cleaned up unused gradle dependencies from root build.gradle. - -## 24.1.3 -* Extended minimum JDK support to 8. - -## 24.1.2 - -* !! Major Breaking Change !! Minimum JDK support is 19 for this minor. - -* Migrated from Sonatype OSSRH. -* Added a new configuration function "addCustomNetworkRequestHeaders(Map)" to add custom request headers to each request. - -## 24.1.1 - -* Added a new function "setID(newDeviceId)" for managing device id changes according to the device ID Type. - -* Mitigated an issue where json and junit dependencies had vulnerabilities. - -## 24.1.0 - -* !! Major breaking change !! The following method and its functionality is deprecated from the "UserEditor" interface and will not function anymore: - * "setLocale(String)" - -* Added the user profiles feature interface, and it is accessible through "Countly::instance()::userProfile()" call. -* Added the location feature interface, and it is accessible through "Countly::instance()::location()" call. -* Added init time configuration for the location parameters: - * "setLocation(String countryCode, String city, String location, String ipAddress)" - * "setDisableLocation()" -* Crash Reporting interface added and accessible through "Countly::instance()::crash()" call. -* Added "disableUnhandledCrashReporting" function to the "Config" class to disable automatic uncaught crash reporting. -* Added "setMaxBreadcrumbCount(int)" function to the "Config" class to change allowed max breadcrumb count. -* Added the views feature interface, and it is accessible through "Countly::instance()::views()" call. -* Added a configuration function to set global view segmentation to the "Config" class: - * "views.setGlobalViewSegmentation(Map)" - -* Fixed a bug where setting custom user properties would not work. -* Fixed a bug where setting organization of the user would not work. -* Fixed a bug where sending a user profile picture with checksum was not possible. -* Fixed a bug where running time calculation was sent as a milliseconds but should have been in seconds. - -* Deprecated "Countly::backendMode()" call, use "Countly::backendM" instead via "instance()" call. -* Deprecated "Usage::addLocation(double, double)" call, use "Countly::location::setLocation" instead via "instance()" call. -* Deprecated "Usage::addCrashReport()" call, use "Countly::crash" instead via "instance()" call. -* The following methods are deprecated from the "UserEditor" interface: - * "commit()" instead use "Countly::userProfile::save" via "instance()" call - * "pushUnique(String, Object)" instead use "Countly::userProfile::pushUnique" via "instance()" call - * "pull(String, Object)" instead use "Countly::userProfile::pull" via "instance()" call - * "push(String, Object)" instead use "Countly::userProfile::push" via "instance()" call - * "setOnce(String, Object)" instead use "Countly::userProfile::setOnce" via "instance()" call - * "max(String, double)" instead use "Countly::userProfile::saveMax" via "instance()" call - * "min(String, double)" instead use "Countly::userProfile::saveMin" via "instance()" call - * "mul(String, double)" instead use "Countly::userProfile::multiply" via "instance()" call - * "inc(String, int)" instead use "Countly::userProfile::incrementBy" via "instance()" call - * "optOutFromLocationServices()" instead use "Countly::location::disableLocation" via "instance()" call - * "setLocation(double, double)" instead use "Countly::location::setLocation" via "instance()" call - * "setLocation(String)" instead use "Countly::location::setLocation" via "instance()" call - * "setCountry(String)" instead use "Countly::location::setLocation" via "instance()" call - * "setCity(String)" instead use "Countly::location::setLocation" via "instance()" call - * "setGender(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setBirthyear(int)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setBirthyear(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setEmail(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setName(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setUsername(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setPhone(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setPicturePath(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setOrg(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setCustom(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "set(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "picture(byte[])" instead use "Countly::userProfile::setProperty" via "instance()" call -* Deprecated "View::start(bool)" call, use "Countly::views::startView" instead via "instance()" call. -* Deprecated "View::stop(bool)" call, use "Countly::views::stopViewWithName" or "Countly::views::stopViewWithID" instead via "instance()" call. -* Deprecated "Usage::view(String)" call, use "Countly::views::startView" instead via "instance()" call. -* Deprecated "Usage::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. -* Deprecated "Countly::view(String)" call, use "Countly::views::startView" instead via "instance()" call. -* Deprecated "Countly::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. - -## 23.10.1 - -* Fixed a bug where getting the feedback widget list would fail if "salt" was enabled. - -## 23.10.0 - -* ! Minor breaking change ! Calling "init" twice will now not reinitialize the SDK. The call will be ignored -* ! Minor breaking change ! 'bounce' and 'exit' segmentation values are now not sent from the SDK. They will be automatically applied on the server. - -* Session update time duration increased to 60 seconds from 30 seconds. -* Adding remaining request queue size information to every request. -* Adding application version information to every request. -* Added the remote config feature. -* Added the Remote Config module with A/B testing. It is accessible through "Countly::instance()::remoteConfig()" call. -* Added configuration functions to configure Remote Config module on init: - * 'enableRemoteConfigValueCaching' to enable caching of remote config values - * 'enrollABOnRCDownload' to enroll A/B tests when remote config values downloaded - * 'enableRemoteConfigAutomaticTriggers' to automatically download remote config values on init - * 'remoteConfigRegisterGlobalCallback(RCDownloadCallback callback)' to register a remote config callback -* Added the ability to set the user profile picture with a URL -* Added the DeviceId interface. It is accessible through "Countly::instance()::deviceId()" call. -* Added a way to get device id type by calling "Countly::deviceId::getType" via "instance()" call -* The SDK now uses a different file for internal configuration. Old file will be deleted. - -* Fixed a bug where it was not possible to send a profile picture with binary data - -* Deprecated following functions from "Usage" interface and respective implementations: - * "changeDeviceIdWithoutMerge" instead use "Countly::deviceId::changeWithoutMerge" via "instance()" call - * "changeDeviceIdWithMerge" instead use "Countly::deviceId::changeWithMerge" via "instance()" call - * "getDeviceId" instead use "Countly::deviceId::getID" via "instance()" call - -## 23.8.0 - -* !! Major breaking change !! The following methods and their functionality are deprecated from the "UserEditor" interface and will not function anymore: - * "addToCohort(key)" - * "removeFromCohort(key)" - -* Added the feedback widget feature. Added consent for it "Config.Feature.Feedback". -* Feedback module is accessible through "Countly::instance()::feedback()" call. - -* Deprecated call "Countly::getSession" is removed -* Deprecated call "resetDeviceId" is removed - -* Deprecated the init time configuration of 'setEventsBufferSize(eventsBufferSize)'. Introduced replacement 'setEventQueueSizeToSend(eventsQueueSize)' -* Deprecated the init time configuration of 'setSendUpdateEachSeconds(sendUpdateEachSeconds)'. Introduced replacement 'setUpdateSessionTimerDelay(delay)' -* In Countly class, the old "init(directory,config)" method is deprecated, use "init(config)" instead via "instance()" call. -* Deprecated "Countly::stop(boolean)" call, use "Countly::halt" or "Countly::stop" instead via "instance()" call. -* Deprecated "Countly::event" call, deprecated builder pattern. Use "Countly::events" instead via "instance()" call. -* Deprecated "Countly::timedEvent(String)" call, use "Countly::events::startEvent" instead via "instance()" call. -* Deprecated "Config::setUsePOST" and "Config::enableUsePOST" calls, use "Config::enableForcedHTTPPost" instead. -* The following methods are deprecated from the "Event" interface: - * "record" - * "endAndRecord" - * "addSegment" - * "addSegments" - * "setSegmentation" - * "setSum" - * "setCount" - * "setDuration" - * "isInvalid" - -## 22.09.2 - -* Fixed internal log calls that did not respect the configured log level and did not work with the log listener. - -## 22.09.1 - -* Adding a way to override metrics sent by "begin session" requests. -* Fixed bug where "setApplicationVersion" would not set the application version in metrics -* ! Minor breaking change ! The following methods and their functionality are deprecated from the "Config" class and will not function anymore: - * "getApplicationName" - * "setApplicationName" - -## 22.09.0 - -* The "resetDeviceId", "login", and "logout" have been deprecated. -* ! Minor breaking change ! The following methods and their functionality are deprecated from the "Config" class and will not function anymore: - * "enableTestMode" - * "disableTestMode" - * "isTestModeEnabled" - * "setLoggingTag" - * "setSdkName" - * "setSdkVersion" - * "getSdkName" - * "getSdkVersion" - * "isDeviceIdFallbackAllowed" - * "setDeviceIdFallbackAllowed" - * "overrideModule" - * "getModuleOverride" - * "getCrashReportingANRCheckingPeriod" - * "setCrashReportingANRCheckingPeriod" - * "disableANRCrashReporting" - -* ! Minor breaking change ! The following methods have been removed from the "Config" class: - * "setAutoViewsTracking" - * "setAutoSessionsTracking" - * "setSessionAutoCloseAfter" - * "isAutoViewsTrackingEnabled" - * "isAutoSessionsTrackingEnabled" - * "getSessionAutoCloseAfter" - * "setSessionCooldownPeriod" - -* ! Minor breaking change ! The "TestMode" functionality is being removed from the SDK. -* ! Minor breaking change ! The module override functionality is being removed from the SDK. -* ! Minor breaking change ! It is not possible to set the logging tag anymore. -* Fixed a bug where the wrong platform field value was being sent in the view request. -* Fixed a bug where view duration was reported in ms and not s. -* Updated JSON library version from "20180813" to "20230227". - -## 20.11.5 - -* Fixed a bug where the backend mode module produces "null pointer exceptions" in case not initialized. - -## 20.11.4 - -* Adding mitigations to an issue that would surface when stopping a view that was not started. - -## 20.11.3 - -* Fixed a threading issue in the backend mode feature. - -## 20.11.2 - -* Added backend mode feature and a new configuration field to enable it. - -## 20.11.1 - -* Fixed a bug related to server response handling. -* Fixed a potential issue with parameters tampering protection while adding checksum. - -## 20.11.0 - -* Added a new method to retrieve the current device id. -* Added new methods to change device ID with and without server merge. -* "Countly::getSession" has been deprecated and this is going to be removed in the future. -* "resetDeviceId" in the SDK public methods has been deprecated and this is going to be removed in the future. - -## 19.09-sdk2-rc - -* initial SDK release -* MavenCentral rerelease - +## XX.XX.XX +* Updated JSON library version from "20250107" to "20250517". + +## 24.1.6 +* Fixed a bug where the request queue would stall after sending the first request, preventing subsequent persisted requests from being sent. + +## 24.1.5 +* Fixed a bug where a non-JSON server response would cause a permanent networking deadlock, preventing all subsequent requests from being sent. +* Fixed a bug where a NullPointerException in SDKCore.recover() would permanently block SDK initialization when a crash file from a previous session existed on disk. + +## 24.1.4 +* ! Minor breaking change ! User properties will now be automatically saved under the following conditions: + * When an event is recorded + * During an internal timer tick + * Upon flushing the event queue + * When a session call made +* Cleaned up unused gradle dependencies from root build.gradle. + +## 24.1.3 +* Extended minimum JDK support to 8. + +## 24.1.2 + +* !! Major Breaking Change !! Minimum JDK support is 19 for this minor. + +* Migrated from Sonatype OSSRH. +* Added a new configuration function "addCustomNetworkRequestHeaders(Map)" to add custom request headers to each request. + +## 24.1.1 + +* Added a new function "setID(newDeviceId)" for managing device id changes according to the device ID Type. + +* Mitigated an issue where json and junit dependencies had vulnerabilities. + +## 24.1.0 + +* !! Major breaking change !! The following method and its functionality is deprecated from the "UserEditor" interface and will not function anymore: + * "setLocale(String)" + +* Added the user profiles feature interface, and it is accessible through "Countly::instance()::userProfile()" call. +* Added the location feature interface, and it is accessible through "Countly::instance()::location()" call. +* Added init time configuration for the location parameters: + * "setLocation(String countryCode, String city, String location, String ipAddress)" + * "setDisableLocation()" +* Crash Reporting interface added and accessible through "Countly::instance()::crash()" call. +* Added "disableUnhandledCrashReporting" function to the "Config" class to disable automatic uncaught crash reporting. +* Added "setMaxBreadcrumbCount(int)" function to the "Config" class to change allowed max breadcrumb count. +* Added the views feature interface, and it is accessible through "Countly::instance()::views()" call. +* Added a configuration function to set global view segmentation to the "Config" class: + * "views.setGlobalViewSegmentation(Map)" + +* Fixed a bug where setting custom user properties would not work. +* Fixed a bug where setting organization of the user would not work. +* Fixed a bug where sending a user profile picture with checksum was not possible. +* Fixed a bug where running time calculation was sent as a milliseconds but should have been in seconds. + +* Deprecated "Countly::backendMode()" call, use "Countly::backendM" instead via "instance()" call. +* Deprecated "Usage::addLocation(double, double)" call, use "Countly::location::setLocation" instead via "instance()" call. +* Deprecated "Usage::addCrashReport()" call, use "Countly::crash" instead via "instance()" call. +* The following methods are deprecated from the "UserEditor" interface: + * "commit()" instead use "Countly::userProfile::save" via "instance()" call + * "pushUnique(String, Object)" instead use "Countly::userProfile::pushUnique" via "instance()" call + * "pull(String, Object)" instead use "Countly::userProfile::pull" via "instance()" call + * "push(String, Object)" instead use "Countly::userProfile::push" via "instance()" call + * "setOnce(String, Object)" instead use "Countly::userProfile::setOnce" via "instance()" call + * "max(String, double)" instead use "Countly::userProfile::saveMax" via "instance()" call + * "min(String, double)" instead use "Countly::userProfile::saveMin" via "instance()" call + * "mul(String, double)" instead use "Countly::userProfile::multiply" via "instance()" call + * "inc(String, int)" instead use "Countly::userProfile::incrementBy" via "instance()" call + * "optOutFromLocationServices()" instead use "Countly::location::disableLocation" via "instance()" call + * "setLocation(double, double)" instead use "Countly::location::setLocation" via "instance()" call + * "setLocation(String)" instead use "Countly::location::setLocation" via "instance()" call + * "setCountry(String)" instead use "Countly::location::setLocation" via "instance()" call + * "setCity(String)" instead use "Countly::location::setLocation" via "instance()" call + * "setGender(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setBirthyear(int)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setBirthyear(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setEmail(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setName(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setUsername(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setPhone(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setPicturePath(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setOrg(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setCustom(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "set(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "picture(byte[])" instead use "Countly::userProfile::setProperty" via "instance()" call +* Deprecated "View::start(bool)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "View::stop(bool)" call, use "Countly::views::stopViewWithName" or "Countly::views::stopViewWithID" instead via "instance()" call. +* Deprecated "Usage::view(String)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Usage::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Countly::view(String)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Countly::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. + +## 23.10.1 + +* Fixed a bug where getting the feedback widget list would fail if "salt" was enabled. + +## 23.10.0 + +* ! Minor breaking change ! Calling "init" twice will now not reinitialize the SDK. The call will be ignored +* ! Minor breaking change ! 'bounce' and 'exit' segmentation values are now not sent from the SDK. They will be automatically applied on the server. + +* Session update time duration increased to 60 seconds from 30 seconds. +* Adding remaining request queue size information to every request. +* Adding application version information to every request. +* Added the remote config feature. +* Added the Remote Config module with A/B testing. It is accessible through "Countly::instance()::remoteConfig()" call. +* Added configuration functions to configure Remote Config module on init: + * 'enableRemoteConfigValueCaching' to enable caching of remote config values + * 'enrollABOnRCDownload' to enroll A/B tests when remote config values downloaded + * 'enableRemoteConfigAutomaticTriggers' to automatically download remote config values on init + * 'remoteConfigRegisterGlobalCallback(RCDownloadCallback callback)' to register a remote config callback +* Added the ability to set the user profile picture with a URL +* Added the DeviceId interface. It is accessible through "Countly::instance()::deviceId()" call. +* Added a way to get device id type by calling "Countly::deviceId::getType" via "instance()" call +* The SDK now uses a different file for internal configuration. Old file will be deleted. + +* Fixed a bug where it was not possible to send a profile picture with binary data + +* Deprecated following functions from "Usage" interface and respective implementations: + * "changeDeviceIdWithoutMerge" instead use "Countly::deviceId::changeWithoutMerge" via "instance()" call + * "changeDeviceIdWithMerge" instead use "Countly::deviceId::changeWithMerge" via "instance()" call + * "getDeviceId" instead use "Countly::deviceId::getID" via "instance()" call + +## 23.8.0 + +* !! Major breaking change !! The following methods and their functionality are deprecated from the "UserEditor" interface and will not function anymore: + * "addToCohort(key)" + * "removeFromCohort(key)" + +* Added the feedback widget feature. Added consent for it "Config.Feature.Feedback". +* Feedback module is accessible through "Countly::instance()::feedback()" call. + +* Deprecated call "Countly::getSession" is removed +* Deprecated call "resetDeviceId" is removed + +* Deprecated the init time configuration of 'setEventsBufferSize(eventsBufferSize)'. Introduced replacement 'setEventQueueSizeToSend(eventsQueueSize)' +* Deprecated the init time configuration of 'setSendUpdateEachSeconds(sendUpdateEachSeconds)'. Introduced replacement 'setUpdateSessionTimerDelay(delay)' +* In Countly class, the old "init(directory,config)" method is deprecated, use "init(config)" instead via "instance()" call. +* Deprecated "Countly::stop(boolean)" call, use "Countly::halt" or "Countly::stop" instead via "instance()" call. +* Deprecated "Countly::event" call, deprecated builder pattern. Use "Countly::events" instead via "instance()" call. +* Deprecated "Countly::timedEvent(String)" call, use "Countly::events::startEvent" instead via "instance()" call. +* Deprecated "Config::setUsePOST" and "Config::enableUsePOST" calls, use "Config::enableForcedHTTPPost" instead. +* The following methods are deprecated from the "Event" interface: + * "record" + * "endAndRecord" + * "addSegment" + * "addSegments" + * "setSegmentation" + * "setSum" + * "setCount" + * "setDuration" + * "isInvalid" + +## 22.09.2 + +* Fixed internal log calls that did not respect the configured log level and did not work with the log listener. + +## 22.09.1 + +* Adding a way to override metrics sent by "begin session" requests. +* Fixed bug where "setApplicationVersion" would not set the application version in metrics +* ! Minor breaking change ! The following methods and their functionality are deprecated from the "Config" class and will not function anymore: + * "getApplicationName" + * "setApplicationName" + +## 22.09.0 + +* The "resetDeviceId", "login", and "logout" have been deprecated. +* ! Minor breaking change ! The following methods and their functionality are deprecated from the "Config" class and will not function anymore: + * "enableTestMode" + * "disableTestMode" + * "isTestModeEnabled" + * "setLoggingTag" + * "setSdkName" + * "setSdkVersion" + * "getSdkName" + * "getSdkVersion" + * "isDeviceIdFallbackAllowed" + * "setDeviceIdFallbackAllowed" + * "overrideModule" + * "getModuleOverride" + * "getCrashReportingANRCheckingPeriod" + * "setCrashReportingANRCheckingPeriod" + * "disableANRCrashReporting" + +* ! Minor breaking change ! The following methods have been removed from the "Config" class: + * "setAutoViewsTracking" + * "setAutoSessionsTracking" + * "setSessionAutoCloseAfter" + * "isAutoViewsTrackingEnabled" + * "isAutoSessionsTrackingEnabled" + * "getSessionAutoCloseAfter" + * "setSessionCooldownPeriod" + +* ! Minor breaking change ! The "TestMode" functionality is being removed from the SDK. +* ! Minor breaking change ! The module override functionality is being removed from the SDK. +* ! Minor breaking change ! It is not possible to set the logging tag anymore. +* Fixed a bug where the wrong platform field value was being sent in the view request. +* Fixed a bug where view duration was reported in ms and not s. +* Updated JSON library version from "20180813" to "20230227". + +## 20.11.5 + +* Fixed a bug where the backend mode module produces "null pointer exceptions" in case not initialized. + +## 20.11.4 + +* Adding mitigations to an issue that would surface when stopping a view that was not started. + +## 20.11.3 + +* Fixed a threading issue in the backend mode feature. + +## 20.11.2 + +* Added backend mode feature and a new configuration field to enable it. + +## 20.11.1 + +* Fixed a bug related to server response handling. +* Fixed a potential issue with parameters tampering protection while adding checksum. + +## 20.11.0 + +* Added a new method to retrieve the current device id. +* Added new methods to change device ID with and without server merge. +* "Countly::getSession" has been deprecated and this is going to be removed in the future. +* "resetDeviceId" in the SDK public methods has been deprecated and this is going to be removed in the future. + +## 19.09-sdk2-rc + +* initial SDK release +* MavenCentral rerelease + diff --git a/app-java/build.gradle b/app-java/build.gradle index 3b90276a..7b39c889 100644 --- a/app-java/build.gradle +++ b/app-java/build.gradle @@ -9,7 +9,7 @@ repositories { } dependencies { - implementation 'org.json:json:20230227' + implementation 'org.json:json:20250517' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(path: ':sdk-java') diff --git a/app-javafx/build.gradle b/app-javafx/build.gradle index d175b4a5..9c8be04c 100644 --- a/app-javafx/build.gradle +++ b/app-javafx/build.gradle @@ -10,7 +10,7 @@ java { } javafx { - version = '17.0.10' + version = '17.0.20' modules = [ 'javafx.controls', 'javafx.web' ] } @@ -21,7 +21,7 @@ dependencies { // org.json is used by the demo for pretty-printing widget data; the // SDK also uses it internally (and exposes JSONObject in its API). - implementation 'org.json:json:20230227' + implementation 'org.json:json:20250517' } application { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8DQ>P;GjjD{w zH}lENr;dU&FbEU?00aa80D$0M0RRB{U*7-#kbjS|qAG&4l5%47zyJ#WrfA#1$1Ctx zf&Z_d{GW=lf^w2#qRJ|CvSJUi(^E3iv~=^Z(zH}F)3Z%V3`@+rNB7gTVU{Bb~90p|f+0(v;nz01EG7yDMX9@S~__vVgv%rS$+?IH+oZ03D5zYrv|^ zC1J)SruYHmCki$jLBlTaE5&dFG9-kq3!^i>^UQL`%gn6)jz54$WDmeYdsBE9;PqZ_ zoGd=P4+|(-u4U1dbAVQrFWoNgNd;0nrghPFbQrJctO>nwDdI`Q^i0XJDUYm|T|RWc zZ3^Qgo_Qk$%Fvjj-G}1NB#ZJqIkh;kX%V{THPqOyiq)d)0+(r9o(qKlSp*hmK#iIY zA^)Vr$-Hz<#SF=0@tL@;dCQsm`V9s1vYNq}K1B)!XSK?=I1)tX+bUV52$YQu*0%fnWEukW>mxkz+%3-S!oguE8u#MGzST8_Dy^#U?fA@S#K$S@9msUiX!gd_ow>08w5)nX{-KxqMOo7d?k2&?Vf z&diGDtZr(0cwPe9z9FAUSD9KC)7(n^lMWuayCfxzy8EZsns%OEblHFSzP=cL6}?J| z0U$H!4S_TVjj<`6dy^2j`V`)mC;cB%* z8{>_%E1^FH!*{>4a7*C1v>~1*@TMcLK{7nEQ!_igZC}ikJ$*<$yHy>7)oy79A~#xE zWavoJOIOC$5b6*q*F_qN1>2#MY)AXVyr$6x4b=$x^*aqF*L?vmj>Mgv+|ITnw_BoW zO?jwHvNy^prH{9$rrik1#fhyU^MpFqF2fYEt(;4`Q&XWOGDH8k6M=%@fics4ajI;st# zCU^r1CK&|jzUhRMv;+W~6N;u<;#DI6cCw-otsc@IsN3MoSD^O`eNflIoR~l4*&-%RBYk@gb^|-JXs&~KuSEmMxB}xSb z@K76cXD=Y|=I&SNC2E+>Zg?R6E%DGCH5J1nU!A|@eX9oS(WPaMm==k2s_ueCqdZw| z&hqHp)47`c{BgwgvY2{xz%OIkY1xDwkw!<0veB#yF4ZKJyabhyyVS`gZepcFIk%e2 zTcrmt2@-8`7i-@5Nz>oQWFuMC_KlroCl(PLSodswHqJ3fn<;gxg9=}~3x_L3P`9Sn zChIf}8vCHvTriz~T2~FamRi?rh?>3bX1j}%bLH+uFX+p&+^aXbOK7clZxdU~6Uxgy z8R=obwO4dL%pmVo*Ktf=lH6hnlz_5k3cG;m8lgaPp~?eD!Yn2kf)tU6PF{kLyn|oI@eQ`F z3IF7~Blqg8-uwUuWZScRKn%c2_}dXB6Dx_&xR*n9M9LXasJhtZdr$vBY!rP{c@=)& z#!?L$2UrkvClwQO>U*fSMs67oSj2mxiJ$t;E|>q%Kh_GzzWWO&3;ufU%2z%ucBU8H z3WIwr$n)cfCXR&>tyB7BcSInK>=ByZA%;cVEJhcg<#6N{aZC4>K41XF>ZgjG`z_u& zGY?;Ad?-sgiOnI`oppF1o1Gurqbi*;#x2>+SSV6|1^G@ooVy@fg?wyf@0Y!UZ4!}nGuLeC^l)6pwkh|oRY`s1Pm$>zZ3u-83T|9 zGaKJIV3_x+u1>cRibsaJpJqhcm%?0-L;2 zitBrdRxNmb0OO2J%Y&Ym(6*`_P3&&5Bw157{o7LFguvxC$4&zTy#U=W*l&(Q2MNO} zfaUwYm{XtILD$3864IA_nn34oVa_g^FRuHL5wdUd)+W-p-iWCKe8m_cMHk+=? zeKX)M?Dt(|{r5t7IenkAXo%&EXIb-i^w+0CX0D=xApC=|Xy(`xy+QG^UyFe z+#J6h_&T5i#sV)hj3D4WN%z;2+jJcZxcI3*CHXGmOF3^)JD5j&wfX)e?-|V0GPuA+ zQFot%aEqGNJJHn$!_}#PaAvQ^{3-Ye7b}rWwrUmX53(|~i0v{}G_sI9uDch_brX&6 zWl5Ndj-AYg(W9CGfQf<6!YmY>Ey)+uYd_JNXH=>|`OH-CDCmcH(0%iD_aLlNHKH z7bcW-^5+QV$jK?R*)wZ>r9t}loM@XN&M-Pw=F#xn(;u3!(3SXXY^@=aoj70;_=QE9 zGghsG3ekq#N||u{4We_25U=y#T*S{4I{++Ku)> zQ!DZW;pVcn>b;&g2;YE#+V`v*Bl&Y-i@X6D*OpNA{G@JAXho&aOk(_j^weW{#3X5Y z%$q_wpb07EYPdmyH(1^09i$ca{O<}7) zRWncXdSPgBE%BM#by!E>tdnc$8RwUJg1*x($6$}ae$e9Knj8gvVZe#bLi!<+&BkFj zg@nOpDneyc+hU9P-;jmOSMN|*H#>^Ez#?;%C3hg_65leSUm;iz)UkW)jX#p)e&S&M z1|a?wDzV5NVnlhRBCd_;F87wp>6c<&nkgvC+!@KGiIqWY4l}=&1w7|r6{oBN8xyzh zG$b#2=RJp_iq6)#t5%yLkKx(0@D=C3w+oiXtSuaQ%I1WIb-eiE$d~!)b@|4XLy!CZ z9p=t=%3ad@Ep+<9003D2KZ5VyP~_n$=;~r&YUg5UZ0KVD&tR1DHy9x)qWtKJp#Kq# zP*8p#W(8JJ_*h_3W}FlvRam?<4Z+-H77^$Lvi+#vmhL9J zJ<1SV45xi;SrO2f=-OB(7#iNA5)x1uNC-yNxUw|!00vcW2PufRm>e~toH;M0Q85MQLWd?3O{i8H+5VkR@l9Dg-ma ze2fZ%>G(u5(k9EHj2L6!;(KZ8%8|*-1V|B#EagbF(rc+5iL_5;Eu)L4Z-V;0HfK4d z*{utLse_rvHZeQ>V5H=f78M3Ntg1BPxFCVD{HbNA6?9*^YIq;B-DJd{Ca2L#)qWP? zvX^NhFmX?CTWw&Ns}lgs;r3i+Bq@y}Ul+U%pzOS0Fcv9~aB(0!>GT0)NO?p=25LjN z2bh>6RhgqD7bQj#k-KOm@JLgMa6>%-ok1WpOe)FS^XOU{c?d5shG(lIn3GiVBxmg`u%-j=)^v&pX1JecJics3&jvPI)mDut52? z3jEA)DM%}BYbxxKrizVYwq?(P&19EXlwD9^-6J+4!}9{ywR9Gk42jjAURAF&EO|~N z)?s>$Da@ikI4|^z0e{r`J8zIs>SpM~Vn^{3fArRu;?+43>lD+^XtUcY1HidJwnR6+ z!;oG2=B6Z_=M%*{z-RaHc(n|1RTKQdNjjV!Pn9lFt^4w|AeN06*j}ZyhqZ^!-=cyGP_ShV1rGxkx8t zB;8`h!S{LD%ot``700d0@Grql(DTt4Awgmi+Yr0@#jbe=2#UkK%rv=OLqF)9D7D1j z!~McAwMYkeaL$~kI~90)5vBhBzWYc3Cj1WI0RS`z000R8-@ET0dA~*r(gSiCJmQMN&4%1D zyVNf0?}sBH8zNbBLn>~(W{d3%@kL_eQ6jEcR{l>C|JK z(R-fA!z|TTRG40|zv}7E@PqCAXP3n`;%|SCQ|ZS%ym$I{`}t3KPL&^l5`3>yah4*6 zifO#{VNz3)?ZL$be;NEaAk9b#{tV?V7 zP|wf5YA*1;s<)9A4~l3BHzG&HH`1xNr#%){4xZ!jq%o=7nN*wMuXlFV{HaiQLJ`5G zBhDi#D(m`Q1pLh@Tq+L;OwuC52RdW7b8}~60WCOK5iYMUad9}7aWBuILb({5=z~YF zt?*Jr5NG+WadM{mDL>GyiByCuR)hd zA=HM?J6l1Xv0Dl+LW@w$OTcEoOda^nFCw*Sy^I@$sSuneMl{4ys)|RY#9&NxW4S)9 zq|%83IpslTLoz~&vTo!Ga@?rj_kw{|k{nv+w&Ku?fyk4Ki4I?);M|5Axm)t+BaE)D zm(`AQ#k^DWrjbuXoJf2{Aj^KT zFb1zMSqxq|vceV+Mf-)$oPflsO$@*A0n0Z!R{&(xh8s}=;t(lIy zv$S8x>m;vQNHuRzoaOo?eiWFe{0;$s`Bc+Osz~}Van${u;g(su`3lJ^TEfo~nERfP z)?aFzpDgnLYiERsKPu|0tq4l2wT)Atr6Qb%m-AUn6HnCue*yWICp7TjW$@sO zm5rm4aTcPQ(rfi7a`xP7cKCFrJD}*&_~xgLyr^-bmsL}y;A5P|al8J3WUoBSjqu%v zxC;mK!g(7r6RRJ852Z~feoC&sD3(6}^5-uLK8o)9{8L_%%rItZK9C){UxB|;G>JbP zsRRtS4-3B*5c+K2kvmgZK8472%l>3cntWUOVHxB|{Ay~aOg5RN;{PJgeVD*H%ac+y!h#wi%o2bF2Ca8IyMyH{>4#{E_8u^@+l-+n=V}Sq?$O z{091@v%Bd*3pk0^2UtiF9Z+(a@wy6 zUdw8J*ze$K#=$48IBi1U%;hmhO>lu!uU;+RS}p&6@rQila7WftH->*A4=5W|Fmtze z)7E}jh@cbmr9iup^i%*(uF%LG&!+Fyl@LFA-}Ca#bxRfDJAiR2dt6644TaYw1Ma79 zt8&DYj31j^5WPNf5P&{)J?WlCe@<3u^78wnd(Ja4^a>{^Tw}W>|Cjt^If|7l^l)^Q zbz|7~CF(k_9~n|h;ysZ+jHzkXf(*O*@5m zLzUmbHp=x!Q|!9NVXyipZ3)^GuIG$k;D)EK!a5=8MFLI_lpf`HPKl=-Ww%z8H_0$j ztJ||IfFG1lE9nmQ0+jPQy zCBdKkjArH@K7jVcMNz);Q(Q^R{d5G?-kk;Uu_IXSyWB)~KGIizZL(^&qF;|1PI7!E zTP`%l)gpX|OFn&)M%txpQ2F!hdA~hX1Cm5)IrdljqzRg!f{mN%G~H1&oqe`5eJCIF zHdD7O;AX-{XEV(a`gBFJ9ews#CVS2y!&>Cm_dm3C8*n3MA*e67(WC?uP@8TXuMroq z{#w$%z@CBIkRM7?}Xib+>hRjy?%G!fiw8! z8(gB+8J~KOU}yO7UGm&1g_MDJ$IXS!`+*b*QW2x)9>K~Y*E&bYMnjl6h!{17_8d!%&9D`a7r&LKZjC<&XOvTRaKJ1 zUY@hl5^R&kZl3lU3njk`3dPzxj$2foOL26r(9zsVF3n_F#v)s5vv3@dgs|lP#eylq62{<-vczqP!RpVBTgI>@O6&sU>W|do17+#OzQ7o5A$ICH z?GqwqnK^n2%LR;$^oZM;)+>$X3s2n}2jZ7CdWIW0lnGK-b#EG01)P@aU`pg}th&J-TrU`tIpb5t((0eu|!u zQz+3ZiOQ^?RxxK4;zs=l8q!-n7X{@jSwK(iqNFiRColuEOg}!7cyZi`iBX4g1pNBj zAPzL?P^Ljhn;1$r8?bc=#n|Ed7wB&oHcw()&*k#SS#h}jO?ZB246EGItsz*;^&tzp zu^YJ0=lwsi`eP_pU8}6JA7MS;9pfD;DsSsLo~ogzMNP70@@;Fm8f0^;>$Z>~}GWRw!W5J3tNX*^2+1f3hz{~rIzJo z6W%J(H!g-eI_J1>0juX$X4Cl6i+3wbc~k146UIX&G22}WE>0ga#WLsn9tY(&29zBvH1$`iWtTe zG2jYl@P!P)eb<5DsR72BdI7-zP&cZNI{7q3e@?N8IKc4DE#UVr->|-ryuJXk^u^>4 z$3wE~=q390;XuOQP~TNoDR?#|NSPJ%sTMInA6*rJ%go|=YjGe!B>z6u$IhgQSwoV* zjy3F2#I>uK{42{&IqP59)Y(1*Z>>#W8rCf4_eVsH)`v!P#^;BgzKDR`ARGEZzkNX+ zJUQu=*-ol=Xqqt5=`=pA@BIn@6a9G8C{c&`i^(i+BxQO9?YZ3iu%$$da&Kb?2kCCo zo7t$UpSFWqmydXf@l3bVJ=%K?SSw)|?srhJ-1ZdFu*5QhL$~-IQS!K1s@XzAtv6*Y zl8@(5BlWYLt1yAWy?rMD&bwze8bC3-GfNH=p zynNFCdxyX?K&G(ZZ)afguQ2|r;XoV^=^(;Cku#qYn4Lus`UeKt6rAlFo_rU`|Rq z&G?~iWMBio<78of-2X(ZYHx~=U0Vz4btyXkctMKdc9UM!vYr~B-(>)(Hc|D zMzkN4!PBg%tZoh+=Gba!0++d193gbMk2&krfDgcbx0jI92cq?FFESVg0D$>F+bil} zY~$)|>1HZsX=5sAZ2WgPB5P=8X#TI+NQ(M~GqyVB53c6IdX=k>Wu@A0Svf5#?uHaF zsYn|koIi3$(%GZ2+G+7Fv^lHTb#5b8sAHSTnL^qWZLM<(1|9|QFw9pnRU{svj}_Al zL)b9>fN{QiA($8peNEJyy`(a{&uh-T4_kdZFIVsKKVM(?05}76EEz?#W za^fiZOAd14IJ4zLX-n7Lq0qlQ^lW8Cvz4UKkV9~P}>sq0?xD3vg+$4vLm~C(+ zM{-3Z#qnZ09bJ>}j?6ry^h+@PfaD7*jZxBEY4)UG&daWb??6)TP+|3#Z&?GL?1i+280CFsE|vIXQbm| zM}Pk!U`U5NsNbyKzkrul-DzwB{X?n3E6?TUHr{M&+R*2%yOiXdW-_2Yd6?38M9Vy^ z*lE%gA{wwoSR~vN0=no}tP2Ul5Gk5M(Xq`$nw#ndFk`tcpd5A=Idue`XZ!FS>Q zG^0w#>P4pPG+*NC9gLP4x2m=cKP}YuS!l^?sHSFftZy{4CoQrb_ z^20(NnG`wAhMI=eq)SsIE~&Gp9Ne0nD4%Xiu|0Fj1UFk?6avDqjdXz{O1nKao*46y zT8~iA%Exu=G#{x=KD;_C&M+Zx4+n`sHT>^>=-1YM;H<72k>$py1?F3#T1*ef9mLZw z5naLQr?n7K;2l+{_uIw*_1nsTn~I|kkCgrn;|G~##hM;9l7Jy$yJfmk+&}W@JeKcF zx@@Woiz8qdi|D%aH3XTx5*wDlbs?dC1_nrFpm^QbG@wM=i2?Zg;$VK!c^Dp8<}BTI zyRhAq@#%2pGV49*Y5_mV4+OICP|%I(dQ7x=6Ob}>EjnB_-_18*xrY?b%-yEDT(wrO z9RY2QT0`_OpGfMObKHV;QLVnrK%mc?$WAdIT`kJQT^n%GuzE7|9@k3ci5fYOh(287 zuIbg!GB3xLg$YN=n)^pHGB0jH+_iIiC=nUcD;G6LuJsjn2VI1cyZx=a?ShCsF==QK z;q~*m&}L<-cb+mDDXzvvrRsybcgQ;Vg21P(uLv5I+eGc7o7tc6`;OA9{soHFOz zT~2?>Ts}gprIX$wRBb4yE>ot<8+*Bv`qbSDv*VtRi|cyWS>)Fjs>fkNOH-+PX&4(~ z&)T8Zam2L6puQl?;5zg9h<}k4#|yH9czHw;1jw-pwBM*O2hUR6yvHATrI%^mvs9q_ z&ccT0>f#eDG<^WG^q@oVqlJrhxH)dcq2cty@l3~|5#UDdExyXUmLQ}f4#;6fI{f^t zDCsgIJ~0`af%YR%Ma5VQq-p21k`vaBu6WE?66+5=XUd%Ay%D$irN>5LhluRWt7 zov-=f>QbMk*G##&DTQyou$s7UqjjW@k6=!I@!k+S{pP8R(2=e@io;N8E`EOB;OGoI zw6Q+{X1_I{OO0HPpBz!X!@`5YQ2)t{+!?M_iH25X(d~-Zx~cXnS9z>u?+If|iNJbx zyFU2d1!ITX64D|lE0Z{dLRqL1Ajj=CCMfC4lD3&mYR_R_VZ>_7_~|<^o*%_&jevU+ zQ4|qzci=0}Jydw|LXLCrOl1_P6Xf@c0$ieK2^7@A9UbF{@V_0p%lqW|L?5k>bVM8|p5v&2g;~r>B8uo<4N+`B zH{J)h;SYiIVx@#jI&p-v3dwL5QNV1oxPr8J%ooezTnLW>i*3Isb49%5i!&ac_dEXv zvXmVUck^QHmyrF8>CGXijC_R-y(Qr{3Zt~EmW)-nC!tiH`wlw5D*W7Pip;T?&j%kX z6DkZX4&}iw>hE(boLyjOoupf6JpvBG8}jIh!!VhnD0>}KSMMo{1#uU6kiFcA04~|7 zVO8eI&x1`g4CZ<2cYUI(n#wz2MtVFHx47yE5eL~8bot~>EHbevSt}LLMQX?odD{Ux zJMnam{d)W4da{l7&y-JrgiU~qY3$~}_F#G7|MxT)e;G{U`In&?`j<5D->}cb{}{T(4DF0BOk-=1195KB-E*o@c?`>y#4=dMtYtSY=&L{!TAjFVcq0y@AH`vH! z$41+u!Ld&}F^COPgL(EE{0X7LY&%D7-(?!kjFF7=qw<;`V{nwWBq<)1QiGJgUc^Vz ztMUlq1bZqKn17|6x6iAHbWc~l1HcmAxr%$Puv!znW)!JiukwIrqQ00|H$Z)OmGG@= zv%A8*4cq}(?qn4rN6o`$Y))(MyXr8R<2S^J+v(wmFmtac!%VOfN?&(8Nr!T@kV`N; z*Q33V3t`^rN&aBiHet)18wy{*wi1=W!B%B-Q6}SCrUl$~Hl{@!95ydml@FK8P=u4s z4e*7gV2s=YxEvskw2Ju!2%{8h01rx-3`NCPc(O zH&J0VH5etNB2KY6k4R@2Wvl^Ck$MoR3=)|SEclT2ccJ!RI9Nuter7u9@;sWf-%um;GfI!=eEIQ2l2p_YWUd{|6EG ze{yO6;lMc>;2tPrsNdi@&1K6(1;|$xe8vLgiouj%QD%gYk`4p{Ktv9|j+!OF-P?@p z;}SV|oIK)iwlBs+`ROXkhd&NK zzo__r!B>tOXpBJMDcv!Mq54P+n4(@dijL^EpO1wdg~q+!DT3lB<>9AANSe!T1XgC=J^)IP0XEZ()_vpu!!3HQyJhwh?r`Ae%Yr~b% zO*NY9t9#qWa@GCPYOF9aron7thfWT`eujS4`t2uG6)~JRTI;f(ZuoRQwjZjp5Pg34 z)rp$)Kr?R+KdJ;IO;pM{$6|2y=k_siqvp%)2||cHTe|b5Ht8&A{wazGNca zX$Ol?H)E_R@SDi~4{d-|8nGFhZPW;Cts1;08TwUvLLv&_2$O6Vt=M)X;g%HUr$&06 zISZb(6)Q3%?;3r~*3~USIg=HcJhFtHhIV(siOwV&QkQe#J%H9&E21!C*d@ln3E@J* zVqRO^<)V^ky-R|%{(9`l-(JXq9J)1r$`uQ8a}$vr9E^nNiI*thK8=&UZ0dsFN_eSl z(q~lnD?EymWLsNa3|1{CRPW60>DSkY9YQ;$4o3W7Ms&@&lv9eH!tk~N&dhqX&>K@} zi1g~GqglxkZ5pEFkllJ)Ta1I^c&Bt6#r(QLQ02yHTaJB~- zCcE=5tmi`UA>@P=1LBfBiqk)HB4t8D?02;9eXj~kVPwv?m{5&!&TFYhu>3=_ zsGmYZ^mo*-j69-42y&Jj0cBLLEulNRZ9vXE)8~mt9C#;tZs;=#M=1*hebkS;7(aGf zcs7zH(I8Eui9UU4L--))yy`&d&$In&VA2?DAEss4LAPCLd>-$i?lpXvn!gu^JJ$(DoUlc6wE98VLZ*z`QGQov5l4Fm_h?V-;mHLYDVOwKz7>e4+%AzeO>P6v}ndPW| zM>m#6Tnp7K?0mbK=>gV}=@k*0Mr_PVAgGMu$j+pWxzq4MAa&jpCDU&-5eH27Iz>m^ zax1?*HhG%pJ((tkR(V(O(L%7v7L%!_X->IjS3H5kuXQT2!ow(;%FDE>16&3r){!ex zhf==oJ!}YU89C9@mfDq!P3S4yx$aGB?rbtVH?sHpg?J5C->!_FHM%Hl3#D4eplxzQ zRA+<@LD%LKSkTk2NyWCg7u=$%F#;SIL44~S_OGR}JqX}X+=bc@swpiClB`Zbz|f!4 z7Ysah7OkR8liXfI`}IIwtEoL}(URrGe;IM8%{>b1SsqXh)~w}P>yiFRaE>}rEnNkT z!HXZUtxUp1NmFm)Dm@-{FI^aRQqpSkz}ZSyKR%Y}YHNzBk)ZIp} zMtS=aMvkgWKm9&oTcU0?S|L~CDqA+sHpOxwnswF-fEG)cXCzUR?ps@tZa$=O)=L+5 zf%m58cq8g_o}3?Bhh+c!w4(7AjxwQ3>WnVi<{{38g7yFboo>q|+7qs<$8CPXUFAN< zG&}BHbbyQ5n|qqSr?U~GY{@GJ{(Jny{bMaOG{|IkUj7tj^9pa9|FB_<+KHLxSxR;@ zHpS$4V)PP+tx}22fWx(Ku9y+}Ap;VZqD0AZW4gCDTPCG=zgJmF{|x;(rvdM|2|9a}cex6xrMkERnkE;}jvU-kmzd%_J50$M`lIPCKf+^*zL=@LW`1SaEc%=m zQ+lT06Gw+wVwvQ9fZ~#qd430v2HndFsBa9WjD0P}K(rZYdAt^5WQIvb%D^Q|pkVE^ zte$&#~zmULFACGfS#g=2OLOnIf2Of-k!(BIHjs77nr!5Q1*I9 z1%?=~#Oss!rV~?-6Gm~BWJiA4mJ5TY&iPm_$)H1_rTltuU1F3I(qTQ^U$S>%$l z)Wx1}R?ij0idp@8w-p!Oz{&*W;v*IA;JFHA9%nUvVDy7Q8woheC#|8QuDZb-L_5@R zOqHwrh|mVL9b=+$nJxM`3eE{O$sCt$UK^2@L$R(r^-_+z?lOo+me-VW=Zw z-Bn>$4ovfWd%SPY`ab-u9{INc*k2h+yH%toDHIyqQ zO68=u`N}RIIs7lsn1D){)~%>ByF<>i@qFb<-axvu(Z+6t7v<^z&gm9McRB~BIaDn$ z#xSGT!rzgad8o>~kyj#h1?7g96tOcCJniQ+*#=b7wPio>|6a1Z?_(TS{)KrPe}(8j z!#&A=k(&Pj^F;r)CI=Z{LVu>uj!_W1q4b`N1}E(i%;BWjbEcnD=mv$FL$l?zS6bW!{$7j1GR5ocn94P2u{ z70tAAcpqtQo<@cXw~@i-@6B23;317|l~S>CB?hR5qJ%J3EFgyBdJd^fHZu7AzHF(BQ!tyAz^L0`X z23S4Fe{2X$W0$zu9gm%rg~A>ijaE#GlYlrF9$ds^QtaszE#4M(OLVP2O-;XdT(XIC zatwzF*)1c+t~c{L=fMG8Z=k5lv>U0;C{caN1NItnuSMp)6G3mbahu>E#sj&oy94KC zpH}8oEw{G@N3pvHhp{^-YaZeH;K+T_1AUv;IKD<=mv^&Ueegrb!yf`4VlRl$M?wsl zZyFol(2|_QM`e_2lYSABpKR{{NlxlDSYQNkS;J66aT#MSiTx~;tUmvs-b*CrR4w=f z8+0;*th6kfZ3|5!Icx3RV11sp=?`0Jy3Fs0N4GZQMN=8HmT6%x9@{Dza)k}UwL6JT zHRDh;%!XwXr6yuuy`4;Xsn0zlR$k%r%9abS1;_v?`HX_hI|+EibVnlyE@3aL5vhQq zlIG?tN^w@0(v9M*&L+{_+RQZw=o|&BRPGB>e5=ys7H`nc8nx)|-g;s7mRc7hg{GJC zAe^vCIJhajmm7C6g! zL&!WAQ~5d_5)00?w_*|*H>3$loHrvFbitw#WvLB!JASO?#5Ig5$Ys10n>e4|3d;tS zELJ0|R4n3Az(Fl3-r^QiV_C;)lQ1_CW{5bKS15U|E9?ZgLec@%kXr84>5jV2a5v=w z?pB1GPdxD$IQL4)G||B_lI+A=08MUFFR4MxfGOu07vfIm+j=z9tp~5i_6jb`tR>qV z$#`=BQ*jpCjm$F0+F)L%xRlnS%#&gro6PiRfu^l!EVan|r3y}AHJQOORGx4~ z&<)3=K-tx518DZyp%|!EqpU!+X3Et7n2AaC5(AtrkW>_57i}$eqs$rupubg0a1+WO zGHZKLN2L0D;ab%{_S1Plm|hx8R?O14*w*f&2&bB050n!R2by zw!@XOQx$SqZ5I<(Qu$V6g>o#A!JVwErWv#(Pjx=KeS0@hxr4?13zj#oWwPS(7Ro|v z>Mp@Kmxo79q|}!5qtX2-O@U&&@6s~!I&)1WQIl?lTnh6UdKT_1R640S4~f=_xoN3- zI+O)$R@RjV$F=>Ti7BlnG1-cFKCC(t|Qjm{SalS~V-tX#+2ekRhwmN zZr`8{QF6y~Z!D|{=1*2D-JUa<(1Z=;!Ei!KiRNH?o{p5o3crFF=_pX9O-YyJchr$~ zRC`+G+8kx~fD2k*ZIiiIGR<8r&M@3H?%JVOfE>)})7ScOd&?OjgAGT@WVNSCZ8N(p zuQG~76GE3%(%h1*vUXg$vH{ua0b`sQ4f0*y=u~lgyb^!#CcPJa2mkSEHGLsnO^kb$ zru5_l#nu=Y{rSMWiYx?nO{8I!gH+?wEj~UM?IrG}E|bRIBUM>UlY<`T1EHpRr36vv zBi&dG8oxS|J$!zoaq{+JpJy+O^W(nt*|#g32bd&K^w-t>!Vu9N!k9eA8r!Xc{utY> zg9aZ(D2E0gL#W0MdjwES-7~Wa8iubPrd?8-$C4BP?*wok&O8+ykOx{P=Izx+G~hM8 z*9?BYz!T8~dzcZr#ux8kS7u7r@A#DogBH8km8Ry4slyie^n|GrTbO|cLhpqgMdsjX zJ_LdmM#I&4LqqsOUIXK8gW;V0B(7^$y#h3h>J0k^WJfAMeYek%Y-Dcb_+0zPJez!GM zAmJ1u;*rK=FNM0Nf}Y!!P9c4)HIkMnq^b;JFd!S3?_Qi2G#LIQ)TF|iHl~WKK6JmK zbv7rPE6VkYr_%_BT}CK8h=?%pk@3cz(UrZ{@h40%XgThP*-Oeo`T0eq9 zA8BnWZKzCy5e&&_GEsU4*;_k}(8l_&al5K-V*BFM=O~;MgRkYsOs%9eOY6s6AtE*<7GQAR2ulC3RAJrG_P1iQK5Z~&B z&f8X<>yJV6)oDGIlS$Y*D^Rj(cszTy5c81a5IwBr`BtnC6_e`ArI8CaTX_%rx7;cn zR-0?J_LFg*?(#n~G8cXut(1nVF0Oka$A$1FGcERU<^ggx;p@CZc?3UB41RY+wLS`LWFNSs~YP zuw1@DNN3lTd|jDL7gjBsd9}wIw}4xT2+8dBQzI00m<@?c2L%>}QLfK5%r!a-iII`p zX@`VEUH)uj^$;7jVUYdADQ2k*!1O3WdfgF?OMtUXNpQ1}QINamBTKDuv19^{$`8A1 zeq%q*O0mi@(%sZU>Xdb0Ru96CFqk9-L3pzLVsMQ`Xpa~N6CR{9Rm2)A|CI21L(%GW zh&)Y$BNHa=FD+=mBw3{qTgw)j0b!Eahs!rZnpu)z!!E$*eXE~##yaXz`KE5(nQM`s zD!$vW9XH)iMxu9R>r$VlLk9oIR%HxpUiW=BK@4U)|1WNQ=mz9a z^!KkO=>GaJ!GBXm{KJj^;kh-MkUlEQ%lza`-G&}C5y1>La1sR6hT=d*NeCnuK%_LV zOXt$}iP6(YJKc9j-Fxq~*ItVUqljQ8?oaysB-EYtFQp9oxZ|5m0^Hq(qV!S+hq#g( z?|i*H2MIr^Kxgz+3vIljQ*Feejy6S4v~jKEPTF~Qhq!(ms5>NGtRgO5vfPPc4Z^AM zTj!`5xEreIN)vaNxa|q6qWdg>+T`Ol0Uz)ckXBXEGvPNEL3R8hB3=C5`@=SYgAju1 z!)UBr{2~=~xa{b8>x2@C7weRAEuatC)3pkRhT#pMPTpSbA|tan%U7NGMvzmF?c!V8 z=pEWxbdXbTAGtWTyI?Fml%lEr-^AE}w#l(<7OIw;ctw}imYax&vR4UYNJZK6P7ZOd zP87XfhnUHxCUHhM@b*NbTi#(-8|wcv%3BGNs#zRCVV(W?1Qj6^PPQa<{yaBwZ`+<`w|;rqUY_C z&AeyKwwf*q#OW-F()lir=T^<^wjK65Lif$puuU5+tk$;e_EJ;Lu+pH>=-8=PDhkBg z8cWt%@$Sc#C6F$Vd+0507;{OOyT7Hs%nKS88q-W!$f~9*WGBpHGgNp}=C*7!RiZ5s zn1L_DbKF@B8kwhDiLKRB@lsXVVLK|ph=w%_`#owlf@s@V(pa`GY$8h%;-#h@TsO|Y8V=n@*!Rog7<7Cid%apR|x zOjhHCyfbIt%+*PCveTEcuiDi%Wx;O;+K=W?OFUV%)%~6;gl?<0%)?snDDqIvkHF{ zyI02)+lI9ov42^hL>ZRrh*HhjF9B$A@=H94iaBESBF=eC_KT$8A@uB^6$~o?3Wm5t1OIaqF^~><2?4e3c&)@wKn9bD? zoeCs;H>b8DL^F&>Xw-xjZEUFFTv>JD^O#1E#)CMBaG4DX9bD(Wtc8Rzq}9soQ8`jf zeSnHOL}<+WVSKp4kkq&?SbETjq6yr@4%SAqOG=9E(3YeLG9dtV+8vmzq+6PFPk{L; z(&d++iu=^F%b+ea$i2UeTC{R*0Isk;vFK!no<;L+(`y`3&H-~VTdKROkdyowo1iqR zbVW(3`+(PQ2>TKY>N!jGmGo7oeoB8O|P_!Ic@ zZ^;3dnuXo;WJ?S+)%P>{Hcg!Jz#2SI(s&dY4QAy_vRlmOh)QHvs_7c&zkJCmJGVvV zX;Mtb>QE+xp`KyciG$Cn*0?AK%-a|=o!+7x&&yzHQOS>8=B*R=niSnta^Pxp1`=md z#;$pS$4WCT?mbiCYU?FcHGZ#)kHVJTTBt^%XE(Q};aaO=Zik0UgLcc0I(tUpt(>|& zcxB_|fxCF7>&~5eJ=Dpn&5Aj{A^cV^^}(7w#p;HG&Q)EaN~~EqrE1qKrMAc&WXIE;>@<&)5;gD2?={Xf@Mvn@OJKw=8Mgn z!JUFMwD+s==JpjhroT&d{$kQAy%+d`a*XxDEVxy3`NHzmITrE`o!;5ClXNPb4t*8P zzAivdr{j_v!=9!^?T3y?gzmqDWX6mkzhIzJ-3S{T5bcCFMr&RPDryMcdwbBuZbsgN zGrp@^i?rcfN7v0NKGzDPGE#4yszxu=I_`MI%Z|10nFjU-UjQXXA?k8Pk|OE<(?ae) zE%vG#eZAlj*E7_3dx#Zz4kMLj>H^;}33UAankJiDy5ZvEhrjr`!9eMD8COp}U*hP+ zF}KIYx@pkccIgyxFm#LNw~G&`;o&5)2`5aogs`1~7cMZQ7zj!%L4E`2yzlQN6REX20&O<9 zKV6fyr)TScJPPzNTC2gL+0x#=u>(({{D7j)c-%tvqls3#Y?Z1m zV5WUE)zdJ{$p>yX;^P!UcXP?UD~YM;IRa#Rs5~l+*$&nO(;Ers`G=0D!twR(0GF@c zHl9E5DQI}Oz74n zfKP>&$q0($T4y$6w(p=ERAFh+>n%iaeRA%!T%<^+pg?M)@ucY<&59$x9M#n+V&>}=nO9wCV{O~lg&v#+jcUj(tQ z`0u1YH)-`U$15a{pBkGyPL0THv1P|4e@pf@3IBZS4dVJPo#H>pWq%Lr0YS-SeWash z8R7=jb28KPMI|_lo#GEO|5B?N_e``H*23{~a!AmUJ+fb4HX-%QI@lSEUxKlGV7z7Q zSKw@-TR>@1RL%w{x}dW#k1NgW+q4yt2Xf1J62Bx*O^WG8OJ|FqI4&@d3_o8Id@*)4 zYrk=>@!wv~mh7YWv*bZhxqSmFh2Xq)o=m;%n$I?GSz49l1$xRpPu_^N(vZ>*>Z<04 z2+rP70oM=NDysd!@fQdM2OcyT?3T^Eb@lIC-UG=Bw{BjQ&P`KCv$AcJ;?`vdZ4){d z&gkoUK{$!$$K`3*O-jyM1~p-7T*qb)Ys>Myt^;#1&a%O@x8A+E>! zY8=eD`ZG)LVagDLBeHg>=atOG?Kr%h4B%E6m@J^C+U|y)XX@f z8oyJDW|9g=<#f<{JRr{y#~euMnv)`7j=%cHWLc}ngjq~7k**6%4u>Px&W%4D94(r* z+akunK}O0DC2A%Xo9jyF;DobX?!1I(7%}@7F>i%&nk*LMO)bMGg2N+1iqtg+r(70q zF5{Msgsm5GS7DT`kBsjMvOrkx&|EU!{{~gL4d2MWrAT=KBQ-^zQCUq{5PD1orxlIL zq;CvlWx#f1NWvh`hg011I%?T_s!e38l*lWVt|~z-PO4~~1g)SrJ|>*tXh=QfXT)%( z+ex+inPvD&O4Ur;JGz>$sUOnWdpSLcm1X%aQDw4{dB!cnj`^muI$CJ2%p&-kULVCE z>$eMR36kN$wCPR+OFDM3-U(VOrp9k3)lI&YVFqd;Kpz~K)@Fa&FRw}L(SoD z9B4a+hQzZT-BnVltst&=kq6Y(f^S4hIGNKYBgMxGJ^;2yrO}P3;r)(-I-CZ)26Y6? z&rzHI_1GCvGkgy-t1E;r^3Le30|%$ebDRu2+gdLG)r=A~Qz`}~&L@aGJ{}vVs_GE* zVUjFnzHiXfKQbpv&bR&}l2bzIjAooB)=-XNcYmrGmBh(&iu@o!^hn0^#}m2yZZUK8 zufVm7Gq0y`Mj;9b>`c?&PZkU0j4>IL=UL&-Lp3j&47B5pAW4JceG{!XCA)kT<%2nqCxj<)uy6XR_uws~>_MEKPOpAQ!H zkn>FKh)<9DwwS*|Y(q?$^N!6(51O0 z^JM~Ax{AI1Oj$fs-S5d4T7Z_i1?{%0SsIuQ&r8#(JA=2iLcTN+?>wOL532%&dMYkT z*T5xepC+V6zxhS@vNbMoi|i)=rpli@R9~P!39tWbSSb904ekv7D#quKbgFEMTb48P zuq(VJ+&L8aWU(_FCD$3^uD!YM%O^K(dvy~Wm2hUuh6bD|#(I39Xt>N1Y{ZqXL`Fg6 zKQ?T2htHN!(Bx;tV2bfTtIj7e)liN-29s1kew>v(D^@)#v;}C4-G=7x#;-dM4yRWm zyY`cS21ulzMK{PoaQ6xChEZ}o_#}X-o}<&0)$1#3we?+QeLt;aVCjeA)hn!}UaKt< zat1fHEx13y-rXNMvpUUmCVzocPmN~-Y4(YJvQ#db)4|%B!rBsgAe+*yor~}FrNH08 z3V!97S}D7d$zbSD{$z;@IYMxM6aHdypIuS*pr_U6;#Y!_?0i|&yU*@16l z*dcMqDQgfNBf}?quiu4e>H)yTVfsp#f+Du0@=Kc41QockXkCkvu>FBd6Q+@FL!(Yx z2`YuX#eMEiLEDhp+9uFqME_E^faV&~9qjBHJkIp~%$x^bN=N)K@kvSVEMdDuzA0sn z88CBG?`RX1@#hQNd`o^V{37)!w|nA)QfiYBE^m=yQKv-fQF+UCMcuEe1d4BH7$?>b zJl-r9@0^Ie=)guO1vOd=i$_4sz>y3x^R7n4ED!5oXL3@5**h(xr%Hv)_gILarO46q+MaDOF%ChaymKoI6JU5Pg;7#2n9-18|S1;AK+ zgsn6;k6-%!QD>D?cFy}8F;r@z8H9xN1jsOBw2vQONVqBVEbkiNUqgw~*!^##ht>w0 zUOykwH=$LwX2j&nLy=@{hr)2O&-wm-NyjW7n~Zs9UlH;P7iP3 zI}S(r0YFVYacnKH(+{*)Tbw)@;6>%=&Th=+Z6NHo_tR|JCI8TJiXv2N7ei7M^Q+RM z?9o`meH$5Yi;@9XaNR#jIK^&{N|DYNNbtdb)XW1Lv2k{E>;?F`#Pq|&_;gm~&~Zc9 zf+6ZE%{x4|{YdtE?a^gKyzr}dA>OxQv+pq|@IXL%WS0CiX!V zm$fCePA%lU{%pTKD7|5NJHeXg=I0jL@$tOF@K*MI$)f?om)D63K*M|r`gb9edD1~Y zc|w7N)Y%do7=0{RC|AziW7#am$)9jciRJ?IWl9PE{G3U+$%FcyKs_0Cgq`=K3@ttV z9g;M!3z~f_?P%y3-ph%vBMeS@p7P&Ea8M@97+%XEj*(1E6vHj==d zjsoviB>j^$_^OI_DEPvFkVo(BGRo%cJeD){6Uckei=~1}>sp299|IRjhXe)%?uP0I zF5+>?0#Ye}T^Y$u_rc4=lPcq4K^D(TZG-w30-YiEM=dcK+4#o*>lJ8&JLi+3UcpZk z!^?95S^C0ja^jwP`|{<+3cBVog$(mRdQmadS+Vh~z zS@|P}=|z3P6uS+&@QsMp0no9Od&27O&14zHXGAOEy zh~OKpymK5C%;LLb467@KgIiVwYbYd6wFxI{0-~MOGfTq$nBTB!{SrWmL9Hs}C&l&l#m?s*{tA?BHS4mVKHAVMqm63H<|c5n0~k)-kbg zXidai&9ZUy0~WFYYKT;oe~rytRk?)r8bptITsWj(@HLI;@=v5|XUnSls7$uaxFRL+ zRVMGuL3w}NbV1`^=Pw*0?>bm8+xfeY(1PikW*PB>>Tq(FR`91N0c2&>lL2sZo5=VD zQY{>7dh_TX98L2)n{2OV=T10~*YzX27i2Q7W86M4$?gZIXZaBq#sA*{PH8){|GUi;oM>e?ua7eF4WFuFYZSG| zze?srg|5Ti8Og{O zeFxuw9!U+zhyk?@w zjsA6(oKD=Ka;A>Ca)oPORxK+kxH#O@zhC!!XS4@=swnuMk>t+JmLmFiE^1aX3f<)D@`%K0FGK^gg1a1j>zi z2KhV>sjU7AX3F$SEqrXSC}fRx64GDoc%!u2Yag68Lw@w9v;xOONf@o)Lc|Uh3<21ctTYu-mFZuHk*+R{GjXHIGq3p)tFtQp%TYqD=j1&y)>@zxoxUJ!G@ zgI0XKmP6MNzw>nRxK$-Gbzs}dyfFzt>#5;f6oR27ql!%+{tr+(`(>%51|k`ML} zY4eE)Lxq|JMas(;JibNQds1bUB&r}ydMQXBY4x(^&fY_&LlQC)3hylc$~8&~|06-D z#T+%66rYbHX%^KuqJED_wuGB+=h`nWA!>1n0)3wZrBG3%`b^Ozv6__dNa@%V14|!D zQ?o$z5u0^8`giv%qE!BzZ!3j;BlDlJDk)h@9{nSQeEk!z9RGW) z${RSF3phEM*ce*>Xdp}585vj$|40=&S{S-GTiE?Op*vY&Lvr9}BO$XWy80IF+6@%n z5*2ueT_g@ofP#u5pxb7n*fv^Xtt7&?SRc{*2Ka-*!BuOpf}neHGCiHy$@Ka1^Dint z;DkmIL$-e)rj4o2WQV%Gy;Xg(_Bh#qeOsTM2f@KEe~4kJ8kNLQ+;(!j^bgJMcNhvklP5Z6I+9Fq@c&D~8Fb-4rmDT!MB5QC{Dsb;BharP*O;SF4& zc$wj-7Oep7#$WZN!1nznc@Vb<_Dn%ga-O#J(l=OGB`dy=Sy&$(5-n3zzu%d7E#^8`T@}V+5B;PP8J14#4cCPw-SQTdGa2gWL0*zKM z#DfSXs_iWOMt)0*+Y>Lkd=LlyoHjublNLefhKBv@JoC>P7N1_#> zv=mLWe96%EY;!ZGSQDbZWb#;tzqAGgx~uk+-$+2_8U`!ypbwXl z^2E-FkM1?lY@yt8=J3%QK+xaZ6ok=-y%=KXCD^0r!5vUneW>95PzCkOPO*t}p$;-> ze5j-BLT_;)cZQzR2CEsm@rU7GZfFtdp*a|g4wDr%8?2QkIGasRfDWT-Dvy*U{?IHT z*}wGnzdlSptl#ZF^sf)KT|BJs&kLG91^A6ls{CzFprZ6-Y!V0Xysh%9p%iMd7HLsS zN+^Un$tDV)T@i!v?3o0Fsx2qI(AX_$dDkBzQ@fRM%n zRXk6hb9Py#JXUs+7)w@eo;g%QQ95Yq!K_d=z{0dGS+pToEI6=Bo8+{k$7&Z zo4>PH(`ce8E-Ps&uv`NQ;U$%t;w~|@E3WVOCi~R4oj5wP?%<*1C%}Jq%a^q~T7u>K zML5AKfQDv6>PuT`{SrKHRAF+^&edg6+5R_#H?Lz3iGoWo#PCEd0DS;)2U({{X#zU^ zw_xv{4x7|t!S)>44J;KfA|DC?;uQ($l+5Vp7oeqf7{GBF9356nx|&B~gs+@N^gSdd zvb*>&W)|u#F{Z_b`f#GVtQ`pYv3#||N{xj1NgB<#=Odt6{eB%#9RLt5v zIi|0u70`#ai}9fJjKv7dE!9ZrOIX!3{$z_K5FBd-Kp-&e4(J$LD-)NMTp^_pB`RT; zftVVlK2g@+1Ahv2$D){@Y#cL#dUj9*&%#6 zd2m9{1NYp>)6=oAvqdCn5#cx{AJ%S8skUgMglu2*IAtd+z1>B&`MuEAS(D(<6X#Lj z?f4CFx$)M&$=7*>9v1ER4b6!SIz-m0e{o0BfkySREchp?WdVPpQCh!q$t>?rL!&Jg zd#heM;&~A}VEm8Dvy&P|J*eAV&w!&Nx6HFV&B8jJFVTmgLaswn!cx$&%JbTsloz!3 zMEz1d`k==`Ueub_JAy_&`!ogbwx27^ZXgFNAbx=g_I~5nO^r)}&myw~+yY*cJl4$I znNJ32M&K=0(2Dj_>@39`3=FX!v3nZHno_@q^!y}%(yw0PqOo=);6Y@&ylVe>nMOZ~ zd>j#QQSBn3oaWd;qy$&5(5H$Ayi)0haAYO6TH>FR?rhqHmNOO+(})NB zLI@B@v0)eq!ug`>G<@htRlp3n!EpU|n+G+AvXFrWSUsLMBfL*ZB`CRsIVHNTR&b?K zxBgsN0BjfB>UVcJ|x%=-zb%OV7lmZc& zxiupadZVF7)6QuhoY;;FK2b*qL0J-Rn-8!X4ZY$-ZSUXV5DFd7`T41c(#lAeLMoeT z4%g655v@7AqT!i@)Edt5JMbN(=Q-6{=L4iG8RA%}w;&pKmtWvI4?G9pVRp|RTw`g0 zD5c12B&A2&P6Ng~8WM2eIW=wxd?r7A*N+&!Be7PX3s|7~z=APxm=A?5 zt>xB4WG|*Td@VX{Rs)PV0|yK`oI3^xn(4c_j&vgxk_Y3o(-`_5o`V zRTghg6%l@(qodXN;dB#+OKJEEvhfcnc#BeO2|E(5df-!fKDZ!%9!^BJ_4)9P+9Dq5 zK1=(v?KmIp34r?z{NEWnLB3Px{XYwy-akun4F7xTRr2^zeYW{gcK9)>aJDdU5;w5@ zak=<+-PLH-|04pelTb%ULpuuuJC7DgyT@D|p{!V!0v3KpDnRjANN12q6SUR3mb9<- z>2r~IApQGhstZ!3*?5V z8#)hJ0TdZg0M-BK#nGFP>$i=qk82DO z7h;Ft!D5E15OgW)&%lej*?^1~2=*Z5$2VX>V{x8SC+{i10BbtUk9@I#Vi&hX)q
Q!LwySI{Bnv%Sm)yh{^sSVJ8&h_D-BJ_YZe5eCaAWU9b$O2c z$T|{vWVRtOL!xC0DTc(Qbe`ItNtt5hr<)VijD0{U;T#bUEp381_y`%ZIav?kuYG{iyYdEBPW=*xNSc;Rlt6~F4M`5G+VtOjc z*0qGzCb@gME5udTjJA-9O<&TWd~}ysBd(eVT1-H82-doyH9RST)|+Pb{o*;$j9Tjs zhU!IlsPsj8=(x3bAKJTopW3^6AKROHR^7wZ185wJGVhA~hEc|LP;k7NEz-@4p5o}F z`AD6naG3(n=NF9HTH81=F+Q|JOz$7wm9I<+#BSmB@o_cLt2GkW9|?7mM;r!JZp89l zbo!Hp8=n!XH1{GwaDU+k)pGp`C|cXkCU5%vcH)+v@0eK>%7gWxmuMu9YLlChA|_D@ zi#5zovN_!a-0?~pUV-Rj*1P)KwdU-LguR>YM&*Nen+ln8Q$?WFCJg%DY%K}2!!1FE zDv-A%Cbwo^p(lzac&_TZ-l#9kq`mhLcY3h9ZTUVCM(Ad&=EriQY5{jJv<5K&g|*Lk zgV%ILnf1%8V2B0E&;Sp4sYbYOvvMebLwYwzkRQ#F8GpTQq#uv=J`uaSJ34OWITeSGo6+-8Xw znCk*n{kdDEi)Hi&u^)~cs@iyCkFWB2SWZU|Uc%^43ZIZQ-vWNExCCtDWjqHs;;tWf$v{}0{p0Rvxkq``)*>+Akq%|Na zA`@~-Vfe|+(AIlqru+7Ceh4nsVmO9p9jc8}HX^W&ViBDXT+uXbT#R#idPn&L>+#b6 zflC-4C5-X;kUnR~L>PSLh*gvL68}RBsu#2l`s_9KjUWRhiqF`j)`y`2`YU(>3bdBj z?>iyjEhe-~$^I5!nn%B6Wh+I`FvLNvauve~eX<+Ipl&04 zT}};W&1a3%W?dJ2=N#0t?e+aK+%t}5q%jSLvp3jZ%?&F}nOOWr>+{GFIa%wO_2`et z=JzoRR~}iKuuR+azPI8;Gf9)z3kyA4EIOSl!sRR$DlW}0>&?GbgPojmjmnln;cTqCt=ADbE zZ8GAnoM+S1(5$i8^O4t`ue;vO4i}z0wz-QEIVe5_u03;}-!G1NyY8;h^}y;tzY}i5 zqQr#Ur3Fy8sSa$Q0ys+f`!`+>9WbvU_I`Sj;$4{S>O3?#inLHCrtLy~!s#WXV=oVP zeE93*Nc`PBi4q@%Ao$x4lw9vLHM!6mn3-b_cebF|n-2vt-zYVF_&sDE--J-P;2WHo z+@n2areE0o$LjvjlV2X7ZU@j+`{*8zq`JR3gKF#EW|#+{nMyo-a>nFFTg&vhyT=b} zDa8+v0(Dgx0yRL@ZXOYIlVSZ0|MFizy0VPW8;AfA5|pe!#j zX}Py^8fl5SyS4g1WSKKtnyP+_PoOwMMwu`(i@Z)diJp~U54*-miOchy7Z35eL>^M z4p<-aIxH4VUZgS783@H%M7P9hX>t{|RU7$n4T(brCG#h9e9p! z+o`i;EGGq3&pF;~5V~eBD}lC)>if$w%Vf}AFxGqO88|ApfHf&Bvu+xdG)@vuF}Yvk z)o;~k-%+0K0g+L`Wala!$=ZV|z$e%>f0%XoLib%)!R^RoS+{!#X?h-6uu zF&&KxORdZU&EwQFITIRLo(7TA3W}y6X{?Y%y2j0It!ekU#<)$qghZtpcS>L3uh`Uj z7GY;6f$9qKynP#oS3$$a{p^{D+0oJQ71`1?OAn_m8)UGZmj3l*ZI)`V-a>MKGGFG< z&^jg#Ok%(hhm>hSrZ5;Qga4u(?^i>GiW_j9%_7M>j(^|Om$#{k+^*ULnEgzW_1gCICtAD^WpC`A z{9&DXkG#01Xo)U$OC(L5Y$DQ|Q4C6CjUKk1UkPj$nXH##J{c8e#K|&{mA*;b$r0E4 zUNo0jthwA(c&N1l=PEe8Rw_8cEl|-eya9z&H3#n`B$t#+aJ03RFMzrV@gowbe8v(c zIFM60^0&lCFO10NU4w@|61xiZ4CVXeaKjd;d?sv52XM*lS8XiVjgWpRB;&U_C0g+`6B5V&w|O6B*_q zsATxL!M}+$He)1eOWECce#eS@2n^xhlB4<_Nn?yCVEQWDs(r`|@2GqLe<#(|&P0U? z$7V5IgpWf09uIf_RazRwC?qEqRaHyL?iiS05UiGesJy%^>-C{{ypTBI&B0-iUYhk> zIk<5xpsuV@g|z(AZD+C-;A!fTG=df1=<%nxy(a(IS+U{ME4ZbDEBtcD_3V=icT6*_ z)>|J?>&6%nvHhZERBtjK+s4xnut*@>GAmA5m*OTp$!^CHTr}vM4n(X1Q*;{e-Rd2BCF-u@1ZGm z!S8hJ6L=Gl4T_SDa7Xx|-{4mxveJg=ctf`BJ*fy!yF6Dz&?w(Q_6B}WQVtNI!BVBC zKfX<>7vd6C96}XAQmF-Jd?1Q4eTfRB3q7hCh0f!(JkdWT5<{iAE#dKy*Jxq&3a1@~ z8C||Dn2mFNyrUV|<-)C^_y7@8c2Fz+2jrae9deBDu;U}tJ{^xAdxCD248(k;dCJ%o z`y3sADe>U%suxwwv~8A1+R$VB=Q?%U?4joI$um;aH+eCrBqpn- z%79D_7rb;R-;-9RTrwi9dPlg8&@tfWhhZ(Vx&1PQ+6(huX`;M9x~LrW~~#3{j0Bh2kDU$}@!fFQej4VGkJv?M4rU^x!RU zEwhu$!CA_iDjFjrJa`aocySDX16?~;+wgav;}Zut6Mg%C4>}8FL?8)Kgwc(Qlj{@#2Pt0?G`$h7P#M+qoXtlV@d}%c&OzO+QYKK`kyXaK{U(O^2DyIXCZlNQjt0^8~8JzNGrIxhj}}M z&~QZlbx%t;MJ(Vux;2tgNKGlAqphLq%pd}JG9uoVHUo?|hN{pLQ6Em%r*+7t^<);X zm~6=qChlNAVXNN*Sow->*4;}T;l;D1I-5T{Bif@4_}=>l`tK;qqDdt5zvisCKhMAH z#r}`)7VW?LZqfdmXQ%zo5bJ00{Xb9^YKrk0Nf|oIW*K@(=`o2Vndz}ZDyk{!u}PVx zzd--+_WC*U{~DH3{?GI64IB+@On&@9X>EUAo&L+G{L^dozaI4C3G#2wr~hseW@K&g zKWs{uHu-9Je!3;4pE>eBltKUXb^*hG8I&413)$J&{D4N%7PcloU6bn%jPxJyQL?g* z9g+YFFEDiE`8rW^laCNzQmi7CTnPfwyg3VDHRAl>h=In6jeaVOP@!-CP60j3+#vpL zEYmh_oP0{-gTe7Or`L6x)6w?77QVi~jD8lWN@3RHcm80iV%M1A!+Y6iHM)05iC64tb$X2lV_%Txk@0l^hZqi^%Z?#- zE;LE0uFx)R08_S-#(wC=dS&}vj6P4>5ZWjhthP=*Hht&TdLtKDR;rXEX4*z0h74FA zMCINqrh3Vq;s%3MC1YL`{WjIAPkVL#3rj^9Pj9Ss7>7duy!9H0vYF%>1jh)EPqvlr6h%R%CxDsk| z!BACz7E%j?bm=pH6Eaw{+suniuY7C9Ut~1cWfOX9KW9=H><&kQlinPV3h9R>3nJvK z4L9(DRM=x;R&d#a@oFY7mB|m8h4692U5eYfcw|QKwqRsshN(q^v$4$)HgPpAJDJ`I zkqjq(8Cd!K!+wCd=d@w%~e$=gdUgD&wj$LQ1r>-E=O@c ze+Z$x{>6(JA-fNVr)X;*)40Eym1TtUZI1Pwwx1hUi+G1Jlk~vCYeXMNYtr)1?qwyg zsX_e*$h?380O00ou?0R@7-Fc59o$UvyVs4cUbujHUA>sH!}L54>`e` zHUx#Q+Hn&Og#YVOuo*niy*GU3rH;%f``nk#NN5-xrZ34NeH$l`4@t);4(+0|Z#I>Y z)~Kzs#exIAaf--65L0UHT_SvV8O2WYeD>Mq^Y6L!Xu8%vnpofG@w!}R7M28?i1*T&zp3X4^OMCY6(Dg<-! zXmcGQrRgHXGYre7GfTJ)rhl|rs%abKT_Nt24_Q``XH{88NVPW+`x4ZdrMuO0iZ0g` z%p}y};~T5gbb9SeL8BSc`SO#ixC$@QhXxZ=B}L`tP}&k?1oSPS=4%{UOHe0<_XWln zwbl5cn(j-qK`)vGHY5B5C|QZd5)W7c@{bNVXqJ!!n$^ufc?N9C-BF2QK1(kv++h!>$QbAjq)_b$$PcJdV+F7hz0Hu@ zqj+}m0qn{t^tD3DfBb~0B36|Q`bs*xs|$i^G4uNUEBl4g;op-;Wl~iThgga?+dL7s zUP(8lMO?g{GcYpDS{NM!UA8Hco?#}eNEioRBHy4`mq!Pd-9@-97|k$hpEX>xoX+dY zDr$wfm^P&}Wu{!%?)U_(%Mn79$(ywvu*kJ9r4u|MyYLI_67U7%6Gd_vb##Nerf@>& z8W11z$$~xEZt$dPG}+*IZky+os5Ju2eRi;1=rUEeIn>t-AzC_IGM-IXWK3^6QNU+2pe=MBn4I*R@A%-iLDCOHTE-O^wo$sL_h{dcPl=^muAQb`_BRm};=cy{qSkui;`WSsj9%c^+bIDQ z0`_?KX0<-=o!t{u(Ln)v>%VGL z0pC=GB7*AQ?N7N{ut*a%MH-tdtNmNC+Yf$|KS)BW(gQJ*z$d{+{j?(e&hgTy^2|AR9vx1Xre2fagGv0YXWqtNkg*v%40v?BJBt|f9wX5 z{QTlCM}b-0{mV?IG>TW_BdviUKhtosrBqdfq&Frdz>cF~yK{P@(w{Vr7z2qKFwLhc zQuogKO@~YwyS9%+d-zD7mJG~@?EFJLSn!a&mhE5$_4xBl&6QHMzL?CdzEnC~C3$X@ zvY!{_GR06ep5;<#cKCSJ%srxX=+pn?ywDwtJ2{TV;0DKBO2t++B(tIO4)Wh`rD13P z4fE$#%zkd=UzOB74gi=-*CuID&Z3zI^-`4U^S?dHxK8fP*;fE|a(KYMgMUo`THIS1f!*6dOI2 zFjC3O=-AL`6=9pp;`CYPTdVX z8(*?V&%QoipuH0>WKlL8A*zTKckD!paN@~hh zmXzm~qZhMGVdQGd=AG8&20HW0RGV8X{$9LldFZYm zE?}`Q3i?xJRz43S?VFMmqRyvWaS#(~Lempg9nTM$EFDP(Gzx#$r)W&lpFKqcAoJh-AxEw$-bjW>`_+gEi z2w`99#UbFZGiQjS8kj~@PGqpsPX`T{YOj`CaEqTFag;$jY z8_{Wzz>HXx&G*Dx<5skhpETxIdhKH?DtY@b9l8$l?UkM#J-Snmts7bd7xayKTFJ(u zyAT&@6cAYcs{PBfpqZa%sxhJ5nSZBPji?Zlf&}#L?t)vC4X5VLp%~fz2Sx<*oN<7` z?ge=k<=X7r<~F7Tvp9#HB{!mA!QWBOf%EiSJ6KIF8QZNjg&x~-%e*tflL(ji_S^sO ztmib1rp09uon}RcsFi#k)oLs@$?vs(i>5k3YN%$T(5Or(TZ5JW9mA6mIMD08=749$ z!d+l*iu{Il7^Yu}H;lgw=En1sJpCKPSqTCHy4(f&NPelr31^*l%KHq^QE>z>Ks_bH zjbD?({~8Din7IvZeJ>8Ey=e;I?thpzD=zE5UHeO|neioJwG;IyLk?xOz(yO&0DTU~ z^#)xcs|s>Flgmp;SmYJ4g(|HMu3v7#;c*Aa8iF#UZo7CvDq4>8#qLJ|YdZ!AsH%^_7N1IQjCro

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fae08049..4f5eb9dc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..23d15a93 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,81 +15,115 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..5eed7ee8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/sdk-java/build.gradle b/sdk-java/build.gradle index a7db0009..1480f0e6 100644 --- a/sdk-java/build.gradle +++ b/sdk-java/build.gradle @@ -22,7 +22,7 @@ java { } dependencies { - implementation 'org.json:json:20250107' + implementation 'org.json:json:20250517' implementation 'com.google.code.findbugs:jsr305:3.0.2' testImplementation 'junit:junit:4.13.1' From f99991ece40cb2ded994140aecdfea5a446b6b0d Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Aug 2026 13:07:17 +0300 Subject: [PATCH 2/7] fix: post --- .github/scripts/osv_scan.py | 31 ++++++-- .github/workflows/dependency-security.yml | 89 +++++++++++++++++++++-- 2 files changed, 108 insertions(+), 12 deletions(-) diff --git a/.github/scripts/osv_scan.py b/.github/scripts/osv_scan.py index 6a27b62b..797eb59d 100755 --- a/.github/scripts/osv_scan.py +++ b/.github/scripts/osv_scan.py @@ -120,8 +120,17 @@ def main(): for ga_v, modules in coords.items(): print(f" ok {ga_v} ({', '.join(modules)})") summary_lines += [ - f"No known vulnerabilities in {len(coords)} resolved dependencies.", - ] + f"No known vulnerabilities in **{len(coords)}** resolved " + f"dependencies across {len(set(m for ms in coords.values() for m in ms))} modules.", + "", + "

Dependencies checked", + "", + "| Dependency | Modules |", + "| --- | --- |", + ] + [ + f"| `{ga_v}` | {', '.join(modules)} |" + for ga_v, modules in coords.items() + ] + ["", "
"] write_summary(summary_lines) return 0 @@ -159,11 +168,19 @@ def main(): def write_summary(lines): - path = os.environ.get("GITHUB_STEP_SUMMARY") - if not path: - return - with open(path, "a", encoding="utf-8") as handle: - handle.write("\n".join(lines) + "\n") + """Write the report to the job summary and to a file for the PR comment. + + GITHUB_STEP_SUMMARY only renders on the workflow run page - it does NOT + reach the pull request. The report file is what the PR comment step posts. + """ + body = "\n".join(lines) + "\n" + step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if step_summary: + with open(step_summary, "a", encoding="utf-8") as handle: + handle.write(body) + with open(os.environ.get("OSV_REPORT_FILE", "osv-report.md"), "w", + encoding="utf-8") as handle: + handle.write(body) if __name__ == "__main__": diff --git a/.github/workflows/dependency-security.yml b/.github/workflows/dependency-security.yml index ec5ebec4..56885f6a 100644 --- a/.github/workflows/dependency-security.yml +++ b/.github/workflows/dependency-security.yml @@ -23,6 +23,10 @@ jobs: osv-scan: name: OSV scan (all modules) runs-on: ubuntu-latest + permissions: + contents: read + # Needed to post the scan result as a comment on the pull request. + pull-requests: write steps: - name: Checkout code uses: actions/checkout@v4 @@ -42,25 +46,90 @@ jobs: - name: Resolve dependencies for every module run: | + set -o pipefail ./gradlew -q --init-script .github/scripts/dependency-report.init.gradle \ printResolvedDependencies | tee resolved-dependencies.txt - grep -c '^COORD' resolved-dependencies.txt + count=$(grep -c '^COORD' resolved-dependencies.txt || true) + echo "Resolved $count coordinate lines." + if [ "$count" -eq 0 ]; then + echo "::error::Gradle produced no dependency coordinates" + exit 1 + fi - name: Check resolved dependencies against OSV + id: osv run: python3 .github/scripts/osv_scan.py < resolved-dependencies.txt + # The job summary written above only shows on the workflow run page. This + # is what actually puts the result on the pull request. It updates one + # sticky comment instead of adding a new one on every push. + - name: Comment scan result on the pull request + if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const outcome = '${{ steps.osv.outcome }}'; + // The report body already carries its own heading. + const status = outcome === 'success' + ? '**Result: passed**' + : '**Result: FAILED — a dependency has a known vulnerability**'; + let report = ''; + try { + report = fs.readFileSync('osv-report.md', 'utf8'); + } catch (e) { + report = `The scan step did not produce a report (outcome: ${outcome}). See the workflow logs.`; + } + const body = [ + marker, + report, + status, + '', + `[Full run log](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`, + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + - name: Upload dependency list if: always() uses: actions/upload-artifact@v4 with: name: resolved-dependencies - path: resolved-dependencies.txt + path: | + resolved-dependencies.txt + osv-report.md dependency-submission: name: Submit dependency graph - # Only from the default branch: this writes the graph GitHub uses for - # Dependabot alerts, and PR runs must not overwrite it. - if: github.event_name == 'push' && github.ref == 'refs/heads/master' + # Deliberately NOT run on pull_request: on that event the ref is + # refs/pull/N/merge, which is not a branch, and the dependency submission + # API expects a real branch. Snapshots are what power Dependabot alerts, + # so they are submitted from the long-lived branches (and on the weekly + # cron, so a newly published advisory is matched against a fresh graph). + # Use workflow_dispatch to submit one on demand from another branch. + if: github.event_name != 'pull_request' runs-on: ubuntu-latest permissions: contents: write @@ -75,8 +144,18 @@ jobs: dependency-review: name: Review dependency changes + # SCOPE: on a Gradle project with no lockfile this reviews the GitHub + # Actions used by the workflows, their licenses and any denied packages -- + # it does NOT see the Gradle dependencies, because that needs a submitted + # snapshot for both the base and the head of the PR. The osv-scan job is + # the check that actually covers the Gradle dependencies on every PR. + # (Do not add `needs: dependency-submission` -- that job is skipped on + # pull_request, and a skipped dependency would skip this job too.) if: github.event_name == 'pull_request' runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - uses: actions/checkout@v4 - name: Fail the PR on newly introduced vulnerable dependencies From 034319cc1da1904ed613f20a0112e88204b795fb Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Aug 2026 13:09:58 +0300 Subject: [PATCH 3/7] fix: post --- .github/workflows/dependency-security.yml | 32 ++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/workflows/dependency-security.yml b/.github/workflows/dependency-security.yml index 56885f6a..91296f7e 100644 --- a/.github/workflows/dependency-security.yml +++ b/.github/workflows/dependency-security.yml @@ -123,13 +123,14 @@ jobs: dependency-submission: name: Submit dependency graph - # Deliberately NOT run on pull_request: on that event the ref is - # refs/pull/N/merge, which is not a branch, and the dependency submission - # API expects a real branch. Snapshots are what power Dependabot alerts, - # so they are submitted from the long-lived branches (and on the weekly - # cron, so a newly published advisory is matched against a fresh graph). - # Use workflow_dispatch to submit one on demand from another branch. - if: github.event_name != 'pull_request' + # Runs on pushes to the long-lived branches, on the weekly cron, on manual + # dispatch, AND on same-repo pull requests -- the last one is what gives + # dependency-review a head snapshot to diff against. + # Fork PRs are skipped on purpose: `contents: write` is not granted to a + # workflow triggered by a PR from a public fork, so the submit would fail. + # Covering forks needs the two-workflow generate-and-upload + workflow_run + # pattern from the gradle/actions docs. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest permissions: contents: write @@ -144,14 +145,15 @@ jobs: dependency-review: name: Review dependency changes - # SCOPE: on a Gradle project with no lockfile this reviews the GitHub - # Actions used by the workflows, their licenses and any denied packages -- - # it does NOT see the Gradle dependencies, because that needs a submitted - # snapshot for both the base and the head of the PR. The osv-scan job is - # the check that actually covers the Gradle dependencies on every PR. - # (Do not add `needs: dependency-submission` -- that job is skipped on - # pull_request, and a skipped dependency would skip this job too.) - if: github.event_name == 'pull_request' + # Needs the submission job so the head snapshot exists before the diff. + # `always()` keeps this running on fork PRs, where submission is skipped -- + # without it, a skipped dependency would skip this job too. + # SCOPE: this can only diff Gradle dependencies once BOTH the base branch + # and the head have a submitted snapshot, so expect Actions-only output + # until master has been through dependency-submission at least once. + # osv-scan is the job that covers Gradle dependencies unconditionally. + if: always() && github.event_name == 'pull_request' + needs: dependency-submission runs-on: ubuntu-latest permissions: contents: read From 55d12facb01654c9e36e2d33d1096e1e4efb6fa4 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Aug 2026 14:30:17 +0300 Subject: [PATCH 4/7] fix: post --- .github/scripts/osv_scan.py | 72 ++++++--- .../ScenarioRequestQueueStallTests.java | 148 ++++++++++++++---- 2 files changed, 174 insertions(+), 46 deletions(-) diff --git a/.github/scripts/osv_scan.py b/.github/scripts/osv_scan.py index 797eb59d..74bbddb8 100755 --- a/.github/scripts/osv_scan.py +++ b/.github/scripts/osv_scan.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Check every resolved Gradle dependency against the OSV vulnerability database. +"""Check resolved Gradle dependencies against the OSV vulnerability DB. Reads "COORD ::" lines (produced by dependency-report.init.gradle) on stdin and queries https://osv.dev. @@ -19,8 +19,12 @@ import urllib.error import urllib.request -OSV_BATCH = "https://api.osv.dev/v1/querybatch" -OSV_VULN = "https://api.osv.dev/v1/vulns/" +OSV_HOST_PREFIX = "https://api.osv.dev/" +OSV_BATCH = OSV_HOST_PREFIX + "v1/querybatch" +OSV_VULN = OSV_HOST_PREFIX + "v1/vulns/" +# Advisory ids come back inside an OSV response, i.e. from outside this repo. +# They are interpolated into a URL, so accept only the documented shape. +VULN_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,100}$") # Local project artifacts have no upstream version to check. SKIP_VERSIONS = {"unspecified", ""} @@ -42,30 +46,49 @@ def read_coords(stream): return {k: sorted(v) for k, v in sorted(coords.items())} +def _check_url(url): + """Reject any URL that is not a plain https OSV API endpoint. + + urlopen would happily accept file:/ or a custom scheme, so the host and + scheme are pinned here rather than trusted from the caller. + """ + if not url.startswith(OSV_HOST_PREFIX): + raise ValueError(f"refusing to fetch a non-OSV URL: {url}") + return url + + def post_json(url, payload): + """POST a JSON payload to OSV and return the decoded response.""" req = urllib.request.Request( - url, + _check_url(url), data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"}, ) - with urllib.request.urlopen(req, timeout=60) as resp: + # nosec B310 - scheme and host are pinned by _check_url above. + with urllib.request.urlopen(req, timeout=60) as resp: # nosec B310 return json.load(resp) def get_json(url): - with urllib.request.urlopen(url, timeout=60) as resp: + """GET an OSV endpoint and return the decoded response.""" + # nosec B310 -- scheme and host are pinned by _check_url above. + checked = _check_url(url) + with urllib.request.urlopen(checked, timeout=60) as resp: # nosec B310 return json.load(resp) def query_osv(coords): - """-> {coord: [vuln id, ...]} for coords with at least one vulnerability.""" + """-> {coord: [vuln id, ...]} for coords with a vulnerability.""" keys = list(coords) queries = [] for ga_v in keys: group, artifact, version = ga_v.rsplit(":", 2) queries.append( { - "package": {"ecosystem": "Maven", "name": f"{group}:{artifact}"}, + "package": { + "ecosystem": "Maven", + "name": f"{group}:{artifact}", + }, "version": version, } ) @@ -80,11 +103,15 @@ def query_osv(coords): def describe(vuln_id): """-> (summary, severity, fixed_versions) - best effort.""" + if not VULN_ID_RE.match(vuln_id): + return "(advisory id in unexpected format)", "", [] try: data = get_json(OSV_VULN + vuln_id) except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError): return "(details unavailable)", "", [] - summary = data.get("summary") or data.get("details", "")[:160] or "(no summary)" + summary = (data.get("summary") + or data.get("details", "")[:160] + or "(no summary)") severity = "" for sev in data.get("severity") or []: score = sev.get("score", "") @@ -106,14 +133,17 @@ def describe(vuln_id): def main(): + """Scan stdin's coordinates and return a process exit code.""" coords = read_coords(sys.stdin) if not coords: - print("::error::no dependency coordinates received - the Gradle report step failed") + print("::error::no dependency coordinates received - the " + "Gradle report step failed") return 1 - print(f"Scanned {len(coords)} resolved dependencies across all modules.\n") + print(f"Scanned {len(coords)} resolved dependencies, all modules.\n") hits = query_osv(coords) + module_count = len({m for ms in coords.values() for m in ms}) summary_lines = ["# Dependency security scan", ""] if not hits: print("No known vulnerabilities.") @@ -121,7 +151,7 @@ def main(): print(f" ok {ga_v} ({', '.join(modules)})") summary_lines += [ f"No known vulnerabilities in **{len(coords)}** resolved " - f"dependencies across {len(set(m for ms in coords.values() for m in ms))} modules.", + f"dependencies across {module_count} modules.", "", "
Dependencies checked", "", @@ -135,7 +165,8 @@ def main(): return 0 summary_lines += [ - f"**{len(hits)} vulnerable dependency(ies)** out of {len(coords)} resolved.", + f"**{len(hits)} vulnerable dependency(ies)** out of " + f"{len(coords)} resolved.", "", "| Dependency | Modules | Advisory | Severity | Fixed in |", "| --- | --- | --- | --- | --- |", @@ -151,17 +182,20 @@ def main(): print(f" fixed in: {fixed_text}") print(f" https://osv.dev/vulnerability/{vuln_id}") # A GitHub annotation so the failure is visible on the PR itself. - print(f"::error title={ga_v} {vuln_id}::{summary} (fixed in {fixed_text})") + print(f"::error title={ga_v} {vuln_id}::{summary} " + f"(fixed in {fixed_text})") summary_lines.append( - f"| `{ga_v}` | {modules} | [{vuln_id}](https://osv.dev/vulnerability/{vuln_id})" + f"| `{ga_v}` | {modules} | " + f"[{vuln_id}](https://osv.dev/vulnerability/{vuln_id})" f" | {severity or 'n/a'} | {fixed_text} |" ) print() summary_lines += [ "", - "Upgrade the dependency in **every** module that declares it - this project " - "declares `org.json` in `sdk-java`, `app-java` and `app-javafx` separately.", + "Upgrade the dependency in **every** module that declares it - " + "this project declares `org.json` in `sdk-java`, `app-java` " + "and `app-javafx` separately.", ] write_summary(summary_lines) return 1 @@ -178,8 +212,8 @@ def write_summary(lines): if step_summary: with open(step_summary, "a", encoding="utf-8") as handle: handle.write(body) - with open(os.environ.get("OSV_REPORT_FILE", "osv-report.md"), "w", - encoding="utf-8") as handle: + report_path = os.environ.get("OSV_REPORT_FILE", "osv-report.md") + with open(report_path, "w", encoding="utf-8") as handle: handle.write(body) diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioRequestQueueStallTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioRequestQueueStallTests.java index 5f4c46f4..b88f9f83 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioRequestQueueStallTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioRequestQueueStallTests.java @@ -1,11 +1,13 @@ package ly.count.sdk.java.internal; import com.sun.net.httpserver.HttpServer; +import java.io.File; import java.io.OutputStream; import java.net.InetSocketAddress; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; import ly.count.sdk.java.Config; import ly.count.sdk.java.Countly; import org.junit.After; @@ -29,12 +31,48 @@ * To reproduce deterministically we hold request #1 open with a CountDownLatch * while we generate a backlog, then release it and assert the backlog drains * without any external trigger. + * + * Timing note: every wait here is a *generous upper bound* on a condition we + * poll for, never a fixed sleep calibrated to one machine. The regression this + * guards against is "the queue stops forever", so a slow CI runner must make + * the test slower, never red. Only the drain assertion is a real assertion — + * the setup waits are preconditions. + * + * Deliberately left on the production session update interval (60s). Shortening + * it makes the SDK's own timer drain the backlog, which would make this test + * pass even with the issue-271 bug present. The callback re-entry path must be + * the only thing that can drain the queue here. */ @RunWith(JUnit4.class) public class ScenarioRequestQueueStallTests { + /** + * Upper bound for a precondition to become true. Not a measurement. + * + * Sized from observed behaviour with a large margin: while a request is in + * flight the synchronous Countly calls in the setup phase have been seen to + * block for ~17s on a developer machine, and the drain runs at roughly one + * request per second. These bounds cost nothing when the test passes -- the + * waits return as soon as their condition holds -- so they are set well + * above the worst case rather than close to the typical case. + */ + private static final long SETUP_TIMEOUT_MS = 60_000; + /** Upper bound for the queue to drain once request #1 is released. */ + private static final long DRAIN_TIMEOUT_MS = 60_000; + /** + * How long the server handler holds request #1 open. Must comfortably + * exceed the whole setup phase: if it expires on its own the request + * completes early and the backlog scenario never happens, which would show + * up as a confusing drain failure rather than an honest timeout. + */ + private static final long HOLD_TIMEOUT_MS = 120_000; + private static final int BACKLOG_SIZE = 5; + private static final int POLL_INTERVAL_MS = 50; + private HttpServer server; private int port; + /** Released in tearDown too, so a failed assertion never leaves the handler thread parked. */ + private final CountDownLatch releaseFirstRequest = new CountDownLatch(1); @Before public void setUp() { @@ -43,6 +81,7 @@ public void setUp() { @After public void tearDown() { + releaseFirstRequest.countDown(); Countly.instance().halt(); if (server != null) { server.stop(0); @@ -57,6 +96,37 @@ private Config configForLocalServer() { .setEventQueueSizeToSend(1); } + /** + * Polls until the condition holds or the budget runs out. + * + * @return true if the condition became true within the budget + */ + private static boolean waitFor(long timeoutMs, BooleanSupplier condition) throws InterruptedException { + long deadline = System.currentTimeMillis() + timeoutMs; + while (System.currentTimeMillis() < deadline) { + if (condition.getAsBoolean()) { + return true; + } + Thread.sleep(POLL_INTERVAL_MS); + } + return condition.getAsBoolean(); + } + + /** Number of persisted request files currently waiting on disk. */ + private static int queuedRequestFileCount() { + File[] files = TestUtils.getTestSDirectory().listFiles(); + if (files == null) { + return 0; + } + int count = 0; + for (File file : files) { + if (file.isFile() && file.getName().startsWith("[CLY]_request_")) { + count++; + } + } + return count; + } + /** * Reproduces the user-reported symptom: with multiple requests piled up * on disk while the network is busy, the queue must drain without @@ -69,7 +139,6 @@ private Config configForLocalServer() { public void backloggedRequests_drainAfterInFlightCompletes() throws Exception { AtomicInteger requestCount = new AtomicInteger(0); CountDownLatch firstRequestArrived = new CountDownLatch(1); - CountDownLatch releaseFirstRequest = new CountDownLatch(1); server = HttpServer.create(new InetSocketAddress(0), 0); port = server.getAddress().getPort(); @@ -79,58 +148,83 @@ public void backloggedRequests_drainAfterInFlightCompletes() throws Exception { // Hold request #1 open until the test has built up a backlog. firstRequestArrived.countDown(); try { - releaseFirstRequest.await(10, TimeUnit.SECONDS); + releaseFirstRequest.await(HOLD_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); } } String body = "{\"result\":\"Success\"}"; - exchange.sendResponseHeaders(200, body.length()); - OutputStream os = exchange.getResponseBody(); - os.write(body.getBytes()); - os.close(); + byte[] bytes = body.getBytes("UTF-8"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } }); server.start(); Countly.instance().init(configForLocalServer()); Countly.session().begin(); - // Wait until request #1 has reached the server and is being held. + // Queue the backlog BEFORE waiting for request #1 to land. + // + // Ordering matters here and getting it wrong is what made this test + // flaky. DefaultNetworking.check() no-ops while config.getDeviceId() is + // still null, so the check() triggered by begin_session can lose that + // startup race. Every subsequently queued request calls check() again + // (SDKCore.onRequest -> Signal.Ping -> checkNetworking), so recording + // the events here guarantees the queue head gets dispatched. Waiting + // for request #1 first instead left the 60s session timer as the only + // retry, which is far longer than any sane test timeout. + // + // Each recordEvent flushes a new request to disk; while the server + // holds #1 they all pile up, because check() short-circuits on + // isRunning() == true. + for (int i = 0; i < BACKLOG_SIZE; i++) { + Countly.instance().events().recordEvent("backlog_evt_" + i); + } + + // Precondition: request #1 reached the server and is being held. Assert.assertTrue( - "request #1 should reach the server within 5s", - firstRequestArrived.await(5, TimeUnit.SECONDS) + "request #1 never reached the local server within " + SETUP_TIMEOUT_MS + + "ms - the SDK never sent anything, so the drain scenario could not be set up", + firstRequestArrived.await(SETUP_TIMEOUT_MS, TimeUnit.MILLISECONDS) ); - // Build up a backlog: each recordEvent flushes a new request to disk. - // While the server holds #1, all of these queue up because - // DefaultNetworking.check() short-circuits on isRunning() == true. - final int backlogSize = 5; - for (int i = 0; i < backlogSize; i++) { - Countly.instance().events().recordEvent("backlog_evt_" + i); - } + // Precondition: the backlog is actually persisted. Waiting on the + // observable state beats sleeping a fixed interval and hoping the + // writes landed - that guess is what made this test runner-dependent. + Assert.assertTrue( + "expected " + BACKLOG_SIZE + " queued request files on disk within " + + SETUP_TIMEOUT_MS + "ms, found " + queuedRequestFileCount(), + waitFor(SETUP_TIMEOUT_MS, () -> queuedRequestFileCount() >= BACKLOG_SIZE) + ); - // Give the event flushes time to actually write request files to disk. - Thread.sleep(500); + // Guard against this test quietly becoming vacuous: if the backlog had + // already drained while #1 was held, the assertion below would pass + // without ever exercising the callback re-entry path. + int expectedMinimum = 1 + BACKLOG_SIZE; + Assert.assertTrue( + "the backlog drained before request #1 was released (" + requestCount.get() + + " requests) - the stall scenario was never set up, so this test would " + + "no longer prove anything about issue #271", + requestCount.get() < expectedMinimum + ); // Release #1. From this point on no external code calls check() — // the queue must self-drain via the callback re-entry path that // issue #271 broke. releaseFirstRequest.countDown(); - // Poll for drain. Total expected = 1 (begin_session) + backlogSize. + // The actual assertion. Total expected = 1 (begin_session) + backlog. // Use >= because device-id resolution or merge requests may add extras; - // the regression is "queue stops at 1", so any number > 1 + a generous - // wait is the meaningful signal. - int expectedMinimum = 1 + backlogSize; - long deadline = System.currentTimeMillis() + 10_000; - while (System.currentTimeMillis() < deadline && requestCount.get() < expectedMinimum) { - Thread.sleep(100); - } + // the regression is "queue stops at 1". + boolean drained = waitFor(DRAIN_TIMEOUT_MS, () -> requestCount.get() >= expectedMinimum); Assert.assertTrue( "request queue should drain to >= " + expectedMinimum + " requests " + "without external check() calls — got " + requestCount.get() + " (queue stalled if << expected)", - requestCount.get() >= expectedMinimum + drained ); } } From c961cbf75089f3e278e7ab7ae744710cf8fab744 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Aug 2026 15:06:20 +0300 Subject: [PATCH 5/7] fix: codacy --- .github/scripts/osv_scan.py | 15 +++++++++------ .../internal/ScenarioRequestQueueStallTests.java | 3 ++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/scripts/osv_scan.py b/.github/scripts/osv_scan.py index 74bbddb8..6ceaccf2 100755 --- a/.github/scripts/osv_scan.py +++ b/.github/scripts/osv_scan.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -"""Check resolved Gradle dependencies against the OSV vulnerability DB. +""" +Check resolved Gradle dependencies against the OSV vulnerability DB. Reads "COORD ::" lines (produced by dependency-report.init.gradle) on stdin and queries https://osv.dev. @@ -30,7 +31,7 @@ def read_coords(stream): - """-> {(group:artifact:version): sorted list of module paths}""" + """Map each coordinate to the sorted module paths that declare it.""" coords = {} for line in stream: parts = line.split() @@ -47,7 +48,8 @@ def read_coords(stream): def _check_url(url): - """Reject any URL that is not a plain https OSV API endpoint. + """ + Reject any URL that is not a plain https OSV API endpoint. urlopen would happily accept file:/ or a custom scheme, so the host and scheme are pinned here rather than trusted from the caller. @@ -78,7 +80,7 @@ def get_json(url): def query_osv(coords): - """-> {coord: [vuln id, ...]} for coords with a vulnerability.""" + """Return {coord: [vuln id, ...]} for coordinates with a vulnerability.""" keys = list(coords) queries = [] for ga_v in keys: @@ -102,7 +104,7 @@ def query_osv(coords): def describe(vuln_id): - """-> (summary, severity, fixed_versions) - best effort.""" + """Return (summary, severity, fixed_versions) for an advisory id.""" if not VULN_ID_RE.match(vuln_id): return "(advisory id in unexpected format)", "", [] try: @@ -202,7 +204,8 @@ def main(): def write_summary(lines): - """Write the report to the job summary and to a file for the PR comment. + """ + Write the report to the job summary and to a file for the PR comment. GITHUB_STEP_SUMMARY only renders on the workflow run page - it does NOT reach the pull request. The report file is what the PR comment step posts. diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioRequestQueueStallTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioRequestQueueStallTests.java index b88f9f83..7dab91b7 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioRequestQueueStallTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioRequestQueueStallTests.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.OutputStream; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -154,7 +155,7 @@ public void backloggedRequests_drainAfterInFlightCompletes() throws Exception { } } String body = "{\"result\":\"Success\"}"; - byte[] bytes = body.getBytes("UTF-8"); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, bytes.length); try (OutputStream os = exchange.getResponseBody()) { os.write(bytes); From fe3d9a3c747921b431576b618d2d82f9ed0b8a21 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Aug 2026 15:40:48 +0300 Subject: [PATCH 6/7] fix: codacy --- CHANGELOG.md | 470 +++++++++++++++++++++++++-------------------------- 1 file changed, 235 insertions(+), 235 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a12ff64..b42b9ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,235 +1,235 @@ -## XX.XX.XX -* Updated JSON library version from "20250107" to "20250517". - -## 24.1.6 -* Fixed a bug where the request queue would stall after sending the first request, preventing subsequent persisted requests from being sent. - -## 24.1.5 -* Fixed a bug where a non-JSON server response would cause a permanent networking deadlock, preventing all subsequent requests from being sent. -* Fixed a bug where a NullPointerException in SDKCore.recover() would permanently block SDK initialization when a crash file from a previous session existed on disk. - -## 24.1.4 -* ! Minor breaking change ! User properties will now be automatically saved under the following conditions: - * When an event is recorded - * During an internal timer tick - * Upon flushing the event queue - * When a session call made -* Cleaned up unused gradle dependencies from root build.gradle. - -## 24.1.3 -* Extended minimum JDK support to 8. - -## 24.1.2 - -* !! Major Breaking Change !! Minimum JDK support is 19 for this minor. - -* Migrated from Sonatype OSSRH. -* Added a new configuration function "addCustomNetworkRequestHeaders(Map)" to add custom request headers to each request. - -## 24.1.1 - -* Added a new function "setID(newDeviceId)" for managing device id changes according to the device ID Type. - -* Mitigated an issue where json and junit dependencies had vulnerabilities. - -## 24.1.0 - -* !! Major breaking change !! The following method and its functionality is deprecated from the "UserEditor" interface and will not function anymore: - * "setLocale(String)" - -* Added the user profiles feature interface, and it is accessible through "Countly::instance()::userProfile()" call. -* Added the location feature interface, and it is accessible through "Countly::instance()::location()" call. -* Added init time configuration for the location parameters: - * "setLocation(String countryCode, String city, String location, String ipAddress)" - * "setDisableLocation()" -* Crash Reporting interface added and accessible through "Countly::instance()::crash()" call. -* Added "disableUnhandledCrashReporting" function to the "Config" class to disable automatic uncaught crash reporting. -* Added "setMaxBreadcrumbCount(int)" function to the "Config" class to change allowed max breadcrumb count. -* Added the views feature interface, and it is accessible through "Countly::instance()::views()" call. -* Added a configuration function to set global view segmentation to the "Config" class: - * "views.setGlobalViewSegmentation(Map)" - -* Fixed a bug where setting custom user properties would not work. -* Fixed a bug where setting organization of the user would not work. -* Fixed a bug where sending a user profile picture with checksum was not possible. -* Fixed a bug where running time calculation was sent as a milliseconds but should have been in seconds. - -* Deprecated "Countly::backendMode()" call, use "Countly::backendM" instead via "instance()" call. -* Deprecated "Usage::addLocation(double, double)" call, use "Countly::location::setLocation" instead via "instance()" call. -* Deprecated "Usage::addCrashReport()" call, use "Countly::crash" instead via "instance()" call. -* The following methods are deprecated from the "UserEditor" interface: - * "commit()" instead use "Countly::userProfile::save" via "instance()" call - * "pushUnique(String, Object)" instead use "Countly::userProfile::pushUnique" via "instance()" call - * "pull(String, Object)" instead use "Countly::userProfile::pull" via "instance()" call - * "push(String, Object)" instead use "Countly::userProfile::push" via "instance()" call - * "setOnce(String, Object)" instead use "Countly::userProfile::setOnce" via "instance()" call - * "max(String, double)" instead use "Countly::userProfile::saveMax" via "instance()" call - * "min(String, double)" instead use "Countly::userProfile::saveMin" via "instance()" call - * "mul(String, double)" instead use "Countly::userProfile::multiply" via "instance()" call - * "inc(String, int)" instead use "Countly::userProfile::incrementBy" via "instance()" call - * "optOutFromLocationServices()" instead use "Countly::location::disableLocation" via "instance()" call - * "setLocation(double, double)" instead use "Countly::location::setLocation" via "instance()" call - * "setLocation(String)" instead use "Countly::location::setLocation" via "instance()" call - * "setCountry(String)" instead use "Countly::location::setLocation" via "instance()" call - * "setCity(String)" instead use "Countly::location::setLocation" via "instance()" call - * "setGender(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setBirthyear(int)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setBirthyear(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setEmail(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setName(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setUsername(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setPhone(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setPicturePath(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setOrg(String)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "setCustom(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "set(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call - * "picture(byte[])" instead use "Countly::userProfile::setProperty" via "instance()" call -* Deprecated "View::start(bool)" call, use "Countly::views::startView" instead via "instance()" call. -* Deprecated "View::stop(bool)" call, use "Countly::views::stopViewWithName" or "Countly::views::stopViewWithID" instead via "instance()" call. -* Deprecated "Usage::view(String)" call, use "Countly::views::startView" instead via "instance()" call. -* Deprecated "Usage::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. -* Deprecated "Countly::view(String)" call, use "Countly::views::startView" instead via "instance()" call. -* Deprecated "Countly::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. - -## 23.10.1 - -* Fixed a bug where getting the feedback widget list would fail if "salt" was enabled. - -## 23.10.0 - -* ! Minor breaking change ! Calling "init" twice will now not reinitialize the SDK. The call will be ignored -* ! Minor breaking change ! 'bounce' and 'exit' segmentation values are now not sent from the SDK. They will be automatically applied on the server. - -* Session update time duration increased to 60 seconds from 30 seconds. -* Adding remaining request queue size information to every request. -* Adding application version information to every request. -* Added the remote config feature. -* Added the Remote Config module with A/B testing. It is accessible through "Countly::instance()::remoteConfig()" call. -* Added configuration functions to configure Remote Config module on init: - * 'enableRemoteConfigValueCaching' to enable caching of remote config values - * 'enrollABOnRCDownload' to enroll A/B tests when remote config values downloaded - * 'enableRemoteConfigAutomaticTriggers' to automatically download remote config values on init - * 'remoteConfigRegisterGlobalCallback(RCDownloadCallback callback)' to register a remote config callback -* Added the ability to set the user profile picture with a URL -* Added the DeviceId interface. It is accessible through "Countly::instance()::deviceId()" call. -* Added a way to get device id type by calling "Countly::deviceId::getType" via "instance()" call -* The SDK now uses a different file for internal configuration. Old file will be deleted. - -* Fixed a bug where it was not possible to send a profile picture with binary data - -* Deprecated following functions from "Usage" interface and respective implementations: - * "changeDeviceIdWithoutMerge" instead use "Countly::deviceId::changeWithoutMerge" via "instance()" call - * "changeDeviceIdWithMerge" instead use "Countly::deviceId::changeWithMerge" via "instance()" call - * "getDeviceId" instead use "Countly::deviceId::getID" via "instance()" call - -## 23.8.0 - -* !! Major breaking change !! The following methods and their functionality are deprecated from the "UserEditor" interface and will not function anymore: - * "addToCohort(key)" - * "removeFromCohort(key)" - -* Added the feedback widget feature. Added consent for it "Config.Feature.Feedback". -* Feedback module is accessible through "Countly::instance()::feedback()" call. - -* Deprecated call "Countly::getSession" is removed -* Deprecated call "resetDeviceId" is removed - -* Deprecated the init time configuration of 'setEventsBufferSize(eventsBufferSize)'. Introduced replacement 'setEventQueueSizeToSend(eventsQueueSize)' -* Deprecated the init time configuration of 'setSendUpdateEachSeconds(sendUpdateEachSeconds)'. Introduced replacement 'setUpdateSessionTimerDelay(delay)' -* In Countly class, the old "init(directory,config)" method is deprecated, use "init(config)" instead via "instance()" call. -* Deprecated "Countly::stop(boolean)" call, use "Countly::halt" or "Countly::stop" instead via "instance()" call. -* Deprecated "Countly::event" call, deprecated builder pattern. Use "Countly::events" instead via "instance()" call. -* Deprecated "Countly::timedEvent(String)" call, use "Countly::events::startEvent" instead via "instance()" call. -* Deprecated "Config::setUsePOST" and "Config::enableUsePOST" calls, use "Config::enableForcedHTTPPost" instead. -* The following methods are deprecated from the "Event" interface: - * "record" - * "endAndRecord" - * "addSegment" - * "addSegments" - * "setSegmentation" - * "setSum" - * "setCount" - * "setDuration" - * "isInvalid" - -## 22.09.2 - -* Fixed internal log calls that did not respect the configured log level and did not work with the log listener. - -## 22.09.1 - -* Adding a way to override metrics sent by "begin session" requests. -* Fixed bug where "setApplicationVersion" would not set the application version in metrics -* ! Minor breaking change ! The following methods and their functionality are deprecated from the "Config" class and will not function anymore: - * "getApplicationName" - * "setApplicationName" - -## 22.09.0 - -* The "resetDeviceId", "login", and "logout" have been deprecated. -* ! Minor breaking change ! The following methods and their functionality are deprecated from the "Config" class and will not function anymore: - * "enableTestMode" - * "disableTestMode" - * "isTestModeEnabled" - * "setLoggingTag" - * "setSdkName" - * "setSdkVersion" - * "getSdkName" - * "getSdkVersion" - * "isDeviceIdFallbackAllowed" - * "setDeviceIdFallbackAllowed" - * "overrideModule" - * "getModuleOverride" - * "getCrashReportingANRCheckingPeriod" - * "setCrashReportingANRCheckingPeriod" - * "disableANRCrashReporting" - -* ! Minor breaking change ! The following methods have been removed from the "Config" class: - * "setAutoViewsTracking" - * "setAutoSessionsTracking" - * "setSessionAutoCloseAfter" - * "isAutoViewsTrackingEnabled" - * "isAutoSessionsTrackingEnabled" - * "getSessionAutoCloseAfter" - * "setSessionCooldownPeriod" - -* ! Minor breaking change ! The "TestMode" functionality is being removed from the SDK. -* ! Minor breaking change ! The module override functionality is being removed from the SDK. -* ! Minor breaking change ! It is not possible to set the logging tag anymore. -* Fixed a bug where the wrong platform field value was being sent in the view request. -* Fixed a bug where view duration was reported in ms and not s. -* Updated JSON library version from "20180813" to "20230227". - -## 20.11.5 - -* Fixed a bug where the backend mode module produces "null pointer exceptions" in case not initialized. - -## 20.11.4 - -* Adding mitigations to an issue that would surface when stopping a view that was not started. - -## 20.11.3 - -* Fixed a threading issue in the backend mode feature. - -## 20.11.2 - -* Added backend mode feature and a new configuration field to enable it. - -## 20.11.1 - -* Fixed a bug related to server response handling. -* Fixed a potential issue with parameters tampering protection while adding checksum. - -## 20.11.0 - -* Added a new method to retrieve the current device id. -* Added new methods to change device ID with and without server merge. -* "Countly::getSession" has been deprecated and this is going to be removed in the future. -* "resetDeviceId" in the SDK public methods has been deprecated and this is going to be removed in the future. - -## 19.09-sdk2-rc - -* initial SDK release -* MavenCentral rerelease - +## XX.XX.XX +* Updated JSON library version from "20250107" to "20250517". + +## 24.1.6 +* Fixed a bug where the request queue would stall after sending the first request, preventing subsequent persisted requests from being sent. + +## 24.1.5 +* Fixed a bug where a non-JSON server response would cause a permanent networking deadlock, preventing all subsequent requests from being sent. +* Fixed a bug where a NullPointerException in SDKCore.recover() would permanently block SDK initialization when a crash file from a previous session existed on disk. + +## 24.1.4 +* ! Minor breaking change ! User properties will now be automatically saved under the following conditions: + * When an event is recorded + * During an internal timer tick + * Upon flushing the event queue + * When a session call made +* Cleaned up unused gradle dependencies from root build.gradle. + +## 24.1.3 +* Extended minimum JDK support to 8. + +## 24.1.2 + +* !! Major Breaking Change !! Minimum JDK support is 19 for this minor. + +* Migrated from Sonatype OSSRH. +* Added a new configuration function "addCustomNetworkRequestHeaders(Map)" to add custom request headers to each request. + +## 24.1.1 + +* Added a new function "setID(newDeviceId)" for managing device id changes according to the device ID Type. + +* Mitigated an issue where json and junit dependencies had vulnerabilities. + +## 24.1.0 + +* !! Major breaking change !! The following method and its functionality is deprecated from the "UserEditor" interface and will not function anymore: + * "setLocale(String)" + +* Added the user profiles feature interface, and it is accessible through "Countly::instance()::userProfile()" call. +* Added the location feature interface, and it is accessible through "Countly::instance()::location()" call. +* Added init time configuration for the location parameters: + * "setLocation(String countryCode, String city, String location, String ipAddress)" + * "setDisableLocation()" +* Crash Reporting interface added and accessible through "Countly::instance()::crash()" call. +* Added "disableUnhandledCrashReporting" function to the "Config" class to disable automatic uncaught crash reporting. +* Added "setMaxBreadcrumbCount(int)" function to the "Config" class to change allowed max breadcrumb count. +* Added the views feature interface, and it is accessible through "Countly::instance()::views()" call. +* Added a configuration function to set global view segmentation to the "Config" class: + * "views.setGlobalViewSegmentation(Map)" + +* Fixed a bug where setting custom user properties would not work. +* Fixed a bug where setting organization of the user would not work. +* Fixed a bug where sending a user profile picture with checksum was not possible. +* Fixed a bug where running time calculation was sent as a milliseconds but should have been in seconds. + +* Deprecated "Countly::backendMode()" call, use "Countly::backendM" instead via "instance()" call. +* Deprecated "Usage::addLocation(double, double)" call, use "Countly::location::setLocation" instead via "instance()" call. +* Deprecated "Usage::addCrashReport()" call, use "Countly::crash" instead via "instance()" call. +* The following methods are deprecated from the "UserEditor" interface: + * "commit()" instead use "Countly::userProfile::save" via "instance()" call + * "pushUnique(String, Object)" instead use "Countly::userProfile::pushUnique" via "instance()" call + * "pull(String, Object)" instead use "Countly::userProfile::pull" via "instance()" call + * "push(String, Object)" instead use "Countly::userProfile::push" via "instance()" call + * "setOnce(String, Object)" instead use "Countly::userProfile::setOnce" via "instance()" call + * "max(String, double)" instead use "Countly::userProfile::saveMax" via "instance()" call + * "min(String, double)" instead use "Countly::userProfile::saveMin" via "instance()" call + * "mul(String, double)" instead use "Countly::userProfile::multiply" via "instance()" call + * "inc(String, int)" instead use "Countly::userProfile::incrementBy" via "instance()" call + * "optOutFromLocationServices()" instead use "Countly::location::disableLocation" via "instance()" call + * "setLocation(double, double)" instead use "Countly::location::setLocation" via "instance()" call + * "setLocation(String)" instead use "Countly::location::setLocation" via "instance()" call + * "setCountry(String)" instead use "Countly::location::setLocation" via "instance()" call + * "setCity(String)" instead use "Countly::location::setLocation" via "instance()" call + * "setGender(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setBirthyear(int)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setBirthyear(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setEmail(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setName(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setUsername(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setPhone(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setPicturePath(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setOrg(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setCustom(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "set(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "picture(byte[])" instead use "Countly::userProfile::setProperty" via "instance()" call +* Deprecated "View::start(bool)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "View::stop(bool)" call, use "Countly::views::stopViewWithName" or "Countly::views::stopViewWithID" instead via "instance()" call. +* Deprecated "Usage::view(String)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Usage::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Countly::view(String)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Countly::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. + +## 23.10.1 + +* Fixed a bug where getting the feedback widget list would fail if "salt" was enabled. + +## 23.10.0 + +* ! Minor breaking change ! Calling "init" twice will now not reinitialize the SDK. The call will be ignored +* ! Minor breaking change ! 'bounce' and 'exit' segmentation values are now not sent from the SDK. They will be automatically applied on the server. + +* Session update time duration increased to 60 seconds from 30 seconds. +* Adding remaining request queue size information to every request. +* Adding application version information to every request. +* Added the remote config feature. +* Added the Remote Config module with A/B testing. It is accessible through "Countly::instance()::remoteConfig()" call. +* Added configuration functions to configure Remote Config module on init: + * 'enableRemoteConfigValueCaching' to enable caching of remote config values + * 'enrollABOnRCDownload' to enroll A/B tests when remote config values downloaded + * 'enableRemoteConfigAutomaticTriggers' to automatically download remote config values on init + * 'remoteConfigRegisterGlobalCallback(RCDownloadCallback callback)' to register a remote config callback +* Added the ability to set the user profile picture with a URL +* Added the DeviceId interface. It is accessible through "Countly::instance()::deviceId()" call. +* Added a way to get device id type by calling "Countly::deviceId::getType" via "instance()" call +* The SDK now uses a different file for internal configuration. Old file will be deleted. + +* Fixed a bug where it was not possible to send a profile picture with binary data + +* Deprecated following functions from "Usage" interface and respective implementations: + * "changeDeviceIdWithoutMerge" instead use "Countly::deviceId::changeWithoutMerge" via "instance()" call + * "changeDeviceIdWithMerge" instead use "Countly::deviceId::changeWithMerge" via "instance()" call + * "getDeviceId" instead use "Countly::deviceId::getID" via "instance()" call + +## 23.8.0 + +* !! Major breaking change !! The following methods and their functionality are deprecated from the "UserEditor" interface and will not function anymore: + * "addToCohort(key)" + * "removeFromCohort(key)" + +* Added the feedback widget feature. Added consent for it "Config.Feature.Feedback". +* Feedback module is accessible through "Countly::instance()::feedback()" call. + +* Deprecated call "Countly::getSession" is removed +* Deprecated call "resetDeviceId" is removed + +* Deprecated the init time configuration of 'setEventsBufferSize(eventsBufferSize)'. Introduced replacement 'setEventQueueSizeToSend(eventsQueueSize)' +* Deprecated the init time configuration of 'setSendUpdateEachSeconds(sendUpdateEachSeconds)'. Introduced replacement 'setUpdateSessionTimerDelay(delay)' +* In Countly class, the old "init(directory,config)" method is deprecated, use "init(config)" instead via "instance()" call. +* Deprecated "Countly::stop(boolean)" call, use "Countly::halt" or "Countly::stop" instead via "instance()" call. +* Deprecated "Countly::event" call, deprecated builder pattern. Use "Countly::events" instead via "instance()" call. +* Deprecated "Countly::timedEvent(String)" call, use "Countly::events::startEvent" instead via "instance()" call. +* Deprecated "Config::setUsePOST" and "Config::enableUsePOST" calls, use "Config::enableForcedHTTPPost" instead. +* The following methods are deprecated from the "Event" interface: + * "record" + * "endAndRecord" + * "addSegment" + * "addSegments" + * "setSegmentation" + * "setSum" + * "setCount" + * "setDuration" + * "isInvalid" + +## 22.09.2 + +* Fixed internal log calls that did not respect the configured log level and did not work with the log listener. + +## 22.09.1 + +* Adding a way to override metrics sent by "begin session" requests. +* Fixed bug where "setApplicationVersion" would not set the application version in metrics +* ! Minor breaking change ! The following methods and their functionality are deprecated from the "Config" class and will not function anymore: + * "getApplicationName" + * "setApplicationName" + +## 22.09.0 + +* The "resetDeviceId", "login", and "logout" have been deprecated. +* ! Minor breaking change ! The following methods and their functionality are deprecated from the "Config" class and will not function anymore: + * "enableTestMode" + * "disableTestMode" + * "isTestModeEnabled" + * "setLoggingTag" + * "setSdkName" + * "setSdkVersion" + * "getSdkName" + * "getSdkVersion" + * "isDeviceIdFallbackAllowed" + * "setDeviceIdFallbackAllowed" + * "overrideModule" + * "getModuleOverride" + * "getCrashReportingANRCheckingPeriod" + * "setCrashReportingANRCheckingPeriod" + * "disableANRCrashReporting" + +* ! Minor breaking change ! The following methods have been removed from the "Config" class: + * "setAutoViewsTracking" + * "setAutoSessionsTracking" + * "setSessionAutoCloseAfter" + * "isAutoViewsTrackingEnabled" + * "isAutoSessionsTrackingEnabled" + * "getSessionAutoCloseAfter" + * "setSessionCooldownPeriod" + +* ! Minor breaking change ! The "TestMode" functionality is being removed from the SDK. +* ! Minor breaking change ! The module override functionality is being removed from the SDK. +* ! Minor breaking change ! It is not possible to set the logging tag anymore. +* Fixed a bug where the wrong platform field value was being sent in the view request. +* Fixed a bug where view duration was reported in ms and not s. +* Updated JSON library version from "20180813" to "20230227". + +## 20.11.5 + +* Fixed a bug where the backend mode module produces "null pointer exceptions" in case not initialized. + +## 20.11.4 + +* Adding mitigations to an issue that would surface when stopping a view that was not started. + +## 20.11.3 + +* Fixed a threading issue in the backend mode feature. + +## 20.11.2 + +* Added backend mode feature and a new configuration field to enable it. + +## 20.11.1 + +* Fixed a bug related to server response handling. +* Fixed a potential issue with parameters tampering protection while adding checksum. + +## 20.11.0 + +* Added a new method to retrieve the current device id. +* Added new methods to change device ID with and without server merge. +* "Countly::getSession" has been deprecated and this is going to be removed in the future. +* "resetDeviceId" in the SDK public methods has been deprecated and this is going to be removed in the future. + +## 19.09-sdk2-rc + +* initial SDK release +* MavenCentral rerelease + From 5a892d2dae0a46b3c9c5ffce146d036a50ea28f9 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Aug 2026 16:22:57 +0300 Subject: [PATCH 7/7] fix: tests --- .github/dependency-scan-allowlist.txt | 8 + .github/scripts/dependency-report.init.gradle | 192 ++++++-- .github/scripts/osv_scan.py | 450 +++++++++++------- .github/workflows/dependency-security.yml | 60 ++- .gitignore | 4 + .../ScenarioNetworkDeadlockTests.java | 10 +- .../ly/count/sdk/java/internal/TestUtils.java | 24 +- 7 files changed, 517 insertions(+), 231 deletions(-) create mode 100644 .github/dependency-scan-allowlist.txt diff --git a/.github/dependency-scan-allowlist.txt b/.github/dependency-scan-allowlist.txt new file mode 100644 index 00000000..8b301cee --- /dev/null +++ b/.github/dependency-scan-allowlist.txt @@ -0,0 +1,8 @@ +# Vulnerabilities the dependency security scan may ignore, one per line: +# +# +# +# The expiry is mandatory. Once it passes, the finding blocks the scan again and the entry +# has to be re-reviewed — a temporary exception must not become a permanent silence. +# Only add an entry when there is a concrete reason the advisory cannot or need not be acted +# on right now, and say what that reason is. diff --git a/.github/scripts/dependency-report.init.gradle b/.github/scripts/dependency-report.init.gradle index 07247450..d2f6c362 100644 --- a/.github/scripts/dependency-report.init.gradle +++ b/.github/scripts/dependency-report.init.gradle @@ -1,34 +1,170 @@ -// Adds a read-only task that prints every resolved external dependency -// coordinate, for every module, as "group:artifact:version". +// Prints every resolved external dependency coordinate, for every module, as +// +// COORD :: // // Applied via `--init-script` so no build file in the repo has to change. -// Used by .github/workflows/dependency-security.yml. -gradle.projectsLoaded { - gradle.rootProject.allprojects { p -> - p.tasks.register('printResolvedDependencies') { - doLast { - def seen = new TreeSet() - p.configurations.each { cfg -> - if (!cfg.canBeResolved) { - return - } - // Skip configurations that pull in the Gradle/plugin classpath - // rather than the shipped or test dependencies. - if (cfg.name.toLowerCase().contains('classpath') && !cfg.name.endsWith('Classpath')) { - return - } - try { - cfg.resolvedConfiguration.lenientConfiguration - .getArtifacts({ true }).each { art -> - def id = art.moduleVersion.id - seen << "${id.group}:${id.name}:${id.version}".toString() - } - } catch (Exception ignored) { - // An unresolvable configuration is not a scan failure. - } - } - seen.each { println "COORD ${p.path} ${it}" } +// Consumed by .github/scripts/osv_scan.py via .github/workflows/dependency-security.yml. +// +// Resolving the real configurations rather than parsing build files is deliberate: it +// captures transitive dependencies and the version conflict resolution actually picks, +// which is what ends up in the artifact. These projects have no Gradle lockfile, so +// lockfile-based scanners see nothing at all. +// +// THREE SCOPES, because a finding's weight depends entirely on who inherits it: +// +// published — on the runtime classpath of a module we publish to Maven Central. +// Every integrator inherits these. Blocks the scan. +// sample — demo applications and test-only dependencies. Never reaches an +// integrator, but it is still our code and our upgrade to make, so it +// blocks too. (This is the scope that catches a vulnerable JSON library +// left behind in a demo module after the SDK itself was fixed.) +// buildscript — the Gradle plugin classpath. Mostly the Android Gradle Plugin's own +// internals, which cannot be upgraded independently of AGP, so these are +// reported and never block. +// +// The published module list is passed in with -DpublishedModules=:a,:b so this file stays +// identical across repositories. Any module that applies the vanniktech publish plugin is +// added automatically, and one that does so without being declared is reported as an error +// rather than being quietly downgraded to `sample`. +// +// Run locally with: +// ./gradlew -I .github/scripts/dependency-report.init.gradle \ +// -DpublishedModules=:sdk,:sdk-native,:upload-plugin printResolvedDependencies + +import org.gradle.api.artifacts.component.ModuleComponentIdentifier + +// Configure-on-demand leaves subprojects unevaluated unless something asks for them, and an +// unevaluated project reports no configurations — the scan would silently cover part of the +// build and still look clean. +gradle.startParameter.configureOnDemand = false + +// Configurations carrying what a consumer of the published artifact actually gets. +def PUBLISHED_CONFIGS = ["runtimeClasspath", "releaseRuntimeClasspath"] as Set + +// Configurations carrying dependencies we declare for demo apps and tests. +// +// This is a closed list on purpose. "Every resolvable configuration that is not a published +// runtime classpath" looks equivalent and is not: the Android Gradle Plugin hangs its own +// tooling configurations off each project (lintChecks, androidTestUtil, androidJdkImage, +// _internal_aapt2_binary, kotlinCompilerClasspath, ...), which drag in netty, bouncycastle +// and jose4j. Treating those as `sample` makes the scan block on AGP internals we cannot +// upgrade -- the very thing the buildscript scope exists to avoid. +// +// Compile classpaths are included as well as runtime ones. A stale declaration can be +// masked at runtime: if a demo module declares an old version of a library that a project +// dependency also supplies at a newer version, `runtimeClasspath` resolves up and the old +// version disappears, while `compileClasspath` still shows what was actually declared. +def SAMPLE_CONFIGS = [ + "runtimeClasspath", + "compileClasspath", + "releaseRuntimeClasspath", + "releaseCompileClasspath", + "debugRuntimeClasspath", + "debugCompileClasspath", + "debugAndroidTestRuntimeClasspath", + "debugAndroidTestCompileClasspath", + "releaseUnitTestRuntimeClasspath", + "debugUnitTestRuntimeClasspath", + "debugUnitTestCompileClasspath", + "testRuntimeClasspath", + "testCompileClasspath", +] as Set + +// Local project artifacts have no upstream version to look up. +def SKIP_VERSIONS = ["unspecified", ""] as Set + +def PUBLISH_PLUGIN = "com.vanniktech.maven.publish" + +gradle.rootProject { rootProject -> + rootProject.tasks.register("printResolvedDependencies") { + group = "verification" + description = "Print every resolved external dependency coordinate for security scanning." + notCompatibleWithConfigurationCache("Resolves every project's configurations at execution time.") + + doLast { + def declared = (System.getProperty("publishedModules") ?: "") + .split(",").collect { it.trim() }.findAll { it } as Set + + // " " -> scope. Each line reports the scope that module + // reaches the coordinate through; the scanner takes the widest scope across lines. + def found = new TreeMap() + def skipped = [] + def undeclared = [] + + def precedence = ["buildscript": 0, "sample": 1, "published": 2] + + def record = { String coordinate, String scope, String modulePath -> + def key = "${modulePath} ${coordinate}".toString() + def existing = found[key] + if (existing == null || precedence[scope] > precedence[existing]) { + found[key] = scope + } + } + + def collectFrom = { configuration, String scope, String modulePath, String label -> + try { + configuration.incoming.resolutionResult.allComponents.each { component -> + def id = component.id + if (id instanceof ModuleComponentIdentifier && !SKIP_VERSIONS.contains(id.version)) { + record("${id.group}:${id.module}:${id.version}".toString(), scope, modulePath) } + } + } catch (Exception e) { + // A configuration that cannot resolve here (missing variant, platform-only, needs + // credentials) must not take the whole scan down, but it must be visible: an + // unreported skip reads as "clean" when it is really "unchecked". + skipped << "${label}: ${e.class.simpleName}: ${e.message?.readLines()?.first()}".toString() + } + } + + rootProject.allprojects.each { project -> + def appliesPublishPlugin = project.plugins.hasPlugin(PUBLISH_PLUGIN) + def isPublished = declared.contains(project.path) || appliesPublishPlugin + if (appliesPublishPlugin && !declared.contains(project.path)) { + undeclared << project.path + } + + project.configurations.each { configuration -> + if (!configuration.canBeResolved) { + return + } + def name = configuration.name + def scope + if (isPublished && PUBLISHED_CONFIGS.contains(name)) { + scope = "published" + } else if (SAMPLE_CONFIGS.contains(name)) { + scope = "sample" + } else { + // An AGP/Gradle tooling configuration. Reported, but not ours to upgrade. + scope = "buildscript" + } + collectFrom(configuration, scope, project.path, "${project.path}:${name}") + } + + def buildscriptClasspath = project.buildscript.configurations.findByName("classpath") + if (buildscriptClasspath != null) { + collectFrom(buildscriptClasspath, "buildscript", project.path, + "${project.path}:buildscript.classpath") } + } + + found.each { key, scope -> + println "COORD ${key} ${scope}" + } + + def distinct = found.keySet().collect { it.split(' ')[1] } as Set + def publishedCoordinates = found.findAll { it.value == "published" } + .keySet().collect { it.split(' ')[1] } as Set + logger.lifecycle("Resolved ${distinct.size()} distinct coordinates " + + "(${publishedCoordinates.size()} published) across ${rootProject.allprojects.size()} modules") + skipped.each { logger.warn("[dependency-report] could not resolve ${it}") } + + if (!undeclared.isEmpty()) { + throw new GradleException( + "These modules apply ${PUBLISH_PLUGIN} but are missing from -DpublishedModules: " + + "${undeclared.join(', ')}. Add them, otherwise their dependencies are scanned as " + + "'sample' and a vulnerability shipped to integrators would not be reported as such.") + } } + } } diff --git a/.github/scripts/osv_scan.py b/.github/scripts/osv_scan.py index 6ceaccf2..adbac0d8 100755 --- a/.github/scripts/osv_scan.py +++ b/.github/scripts/osv_scan.py @@ -1,223 +1,317 @@ #!/usr/bin/env python3 -""" -Check resolved Gradle dependencies against the OSV vulnerability DB. +"""Check resolved Gradle dependencies against the OSV vulnerability database. -Reads "COORD ::" lines (produced by +Reads "COORD :: " lines (produced by dependency-report.init.gradle) on stdin and queries https://osv.dev. -Exits 1 if anything vulnerable is found, so CI fails the job. +Why this rather than a stock scanner: these projects have no Gradle lockfile, so +lockfile-based scanners see nothing. Scanning the *resolved* graph of every module is +what catches a dependency that only appears transitively, or one that was upgraded in +the SDK but left behind in a demo module. + +Scopes, and which of them fail the run: + + published ships to integrators -> blocks + sample demo apps and test-only deps -> blocks (ours to fix, just not shipped) + buildscript Gradle plugin classpath -> reported, never blocks + +`buildscript` is exempt because it is largely the Android Gradle Plugin's own transitive +internals, which cannot be upgraded independently of AGP. Blocking on them would make the +job permanently red, and a permanently red check is one nobody reads. -Why this exists rather than a stock scanner: this project has no Gradle -lockfile, so lockfile-based scanners see nothing. It also declares org.json -separately in sdk-java, app-java and app-javafx -- a past upgrade bumped only -sdk-java and left the demo modules on a version with a known CVE. Scanning the -*resolved* graph of every module is what catches that. + python3 .github/scripts/osv_scan.py < resolved-dependencies.txt + +Options come from the environment so the workflow stays declarative: + OSV_FAIL_ON lowest severity that fails the run (default HIGH) + OSV_ALLOWLIST allowlist file (default .github/dependency-scan-allowlist.txt) + OSV_BLOCKING_SCOPES comma-separated scopes that may fail the run + (default "published,sample") + OSV_REPORT_FILE markdown report for the PR comment (default osv-report.md) + +OSV needs no API key, so this runs on forks and without repository secrets. """ + +import datetime import json import os import re import sys +import time import urllib.error import urllib.request OSV_HOST_PREFIX = "https://api.osv.dev/" -OSV_BATCH = OSV_HOST_PREFIX + "v1/querybatch" -OSV_VULN = OSV_HOST_PREFIX + "v1/vulns/" -# Advisory ids come back inside an OSV response, i.e. from outside this repo. -# They are interpolated into a URL, so accept only the documented shape. +OSV_BATCH_URL = OSV_HOST_PREFIX + "v1/querybatch" +OSV_VULN_URL = OSV_HOST_PREFIX + "v1/vulns/" +BATCH_SIZE = 100 +SEVERITY_ORDER = ["UNKNOWN", "LOW", "MODERATE", "HIGH", "CRITICAL"] +SCOPE_PRECEDENCE = {"buildscript": 0, "sample": 1, "published": 2} +KNOWN_SCOPES = set(SCOPE_PRECEDENCE) + +# Advisory ids arrive inside an OSV response, i.e. from outside this repo, and are +# interpolated into a URL. Accept only the documented shape. VULN_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,100}$") -# Local project artifacts have no upstream version to check. -SKIP_VERSIONS = {"unspecified", ""} -def read_coords(stream): - """Map each coordinate to the sorted module paths that declare it.""" - coords = {} - for line in stream: - parts = line.split() - if len(parts) != 3 or parts[0] != "COORD": - continue - _, project, ga_v = parts - if ga_v.count(":") != 2: - continue - version = ga_v.rsplit(":", 1)[1] - if version in SKIP_VERSIONS: - continue - coords.setdefault(ga_v, set()).add(project) - return {k: sorted(v) for k, v in sorted(coords.items())} +def rank(severity): + try: + return SEVERITY_ORDER.index(severity) + except ValueError: + return 0 def _check_url(url): - """ - Reject any URL that is not a plain https OSV API endpoint. + """Reject any URL that is not a plain https OSV endpoint. - urlopen would happily accept file:/ or a custom scheme, so the host and - scheme are pinned here rather than trusted from the caller. + urlopen would happily accept file:/ or a custom scheme, so the host and scheme are + pinned here rather than trusted from the caller. """ if not url.startswith(OSV_HOST_PREFIX): raise ValueError(f"refusing to fetch a non-OSV URL: {url}") return url -def post_json(url, payload): - """POST a JSON payload to OSV and return the decoded response.""" - req = urllib.request.Request( - _check_url(url), - data=json.dumps(payload).encode(), - headers={"Content-Type": "application/json"}, - ) - # nosec B310 - scheme and host are pinned by _check_url above. - with urllib.request.urlopen(req, timeout=60) as resp: # nosec B310 - return json.load(resp) +def post_json(url, payload, attempts=4): + """POST with retries. A network failure must abort the scan, never quietly pass it.""" + body = json.dumps(payload).encode() + request = urllib.request.Request(_check_url(url), body, {"Content-Type": "application/json"}) + return _send(request, url, attempts) + + +def get_json(url, attempts=4): + return _send(urllib.request.Request(_check_url(url)), url, attempts) + + +def _send(request, url, attempts): + last_error = None + for attempt in range(attempts): + try: + with urllib.request.urlopen(request, timeout=60) as response: + return json.load(response) + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error: + last_error = error + if attempt < attempts - 1: + time.sleep(2 ** attempt) + raise SystemExit(f"error: could not reach OSV at {url}: {last_error}") + +def read_coordinates(stream): + """Return {coordinate: {"scope": str, "modules": [str, ...]}}.""" + entries = {} + for line in stream: + parts = line.split() + if len(parts) != 4 or parts[0] != "COORD": + continue + _, module, coordinate, scope = parts + if coordinate.count(":") != 2: + print(f"warning: skipping unparseable coordinate {coordinate!r}", file=sys.stderr) + continue + if scope not in KNOWN_SCOPES: + raise SystemExit(f"error: unknown scope {scope!r} for {coordinate}") -def get_json(url): - """GET an OSV endpoint and return the decoded response.""" - # nosec B310 -- scheme and host are pinned by _check_url above. - checked = _check_url(url) - with urllib.request.urlopen(checked, timeout=60) as resp: # nosec B310 - return json.load(resp) + entry = entries.setdefault(coordinate, {"scope": scope, "modules": set()}) + entry["modules"].add(module) + # The widest-reaching scope wins: an artifact reached both ways still ships. + if SCOPE_PRECEDENCE[scope] > SCOPE_PRECEDENCE[entry["scope"]]: + entry["scope"] = scope + return {c: {"scope": e["scope"], "modules": sorted(e["modules"])} + for c, e in sorted(entries.items())} -def query_osv(coords): - """Return {coord: [vuln id, ...]} for coordinates with a vulnerability.""" - keys = list(coords) + +def read_allowlist(path): + """Parse " " lines. + + An expiry is mandatory. An entry that has passed its date stops suppressing its + finding, so a temporary exception cannot quietly become permanent. + """ + allowed = {} + if not os.path.exists(path): + return allowed + today = datetime.date.today() + with open(path) as handle: + for number, line in enumerate(handle, 1): + line = line.split("#", 1)[0].strip() + if not line: + continue + parts = line.split(None, 2) + if len(parts) < 3: + raise SystemExit( + f"error: {path}:{number}: expected ' ', " + f"got {line!r}") + vuln_id, expiry_text, reason = parts + try: + expiry = datetime.date.fromisoformat(expiry_text) + except ValueError: + raise SystemExit(f"error: {path}:{number}: {expiry_text!r} is not a YYYY-MM-DD date") + allowed[vuln_id] = {"expiry": expiry, "reason": reason, "expired": expiry < today} + return allowed + + +def query_osv(coordinates): + """Return {coordinate: [vuln id, ...]} for coordinates OSV knows something about.""" queries = [] - for ga_v in keys: - group, artifact, version = ga_v.rsplit(":", 2) - queries.append( - { - "package": { - "ecosystem": "Maven", - "name": f"{group}:{artifact}", - }, - "version": version, - } - ) - results = post_json(OSV_BATCH, {"queries": queries})["results"] + for coordinate in coordinates: + group, name, version = coordinate.split(":") + queries.append({"version": version, + "package": {"name": f"{group}:{name}", "ecosystem": "Maven"}}) + + results = [] + for start in range(0, len(queries), BATCH_SIZE): + chunk = queries[start:start + BATCH_SIZE] + batch = post_json(OSV_BATCH_URL, {"queries": chunk}).get("results", []) + if len(batch) != len(chunk): + raise SystemExit( + f"error: OSV returned {len(batch)} results for {len(chunk)} queries; " + "refusing to report a partial scan as clean") + results.extend(batch) + hits = {} - for ga_v, result in zip(keys, results): - ids = [v["id"] for v in result.get("vulns", [])] + for coordinate, result in zip(coordinates, results): + ids = [] + for vuln in result.get("vulns", []): + vuln_id = vuln.get("id", "") + if VULN_ID_RE.match(vuln_id): + ids.append(vuln_id) + else: + print(f"warning: ignoring malformed advisory id {vuln_id!r}", file=sys.stderr) if ids: - hits[ga_v] = ids + hits[coordinate] = ids return hits -def describe(vuln_id): - """Return (summary, severity, fixed_versions) for an advisory id.""" - if not VULN_ID_RE.match(vuln_id): - return "(advisory id in unexpected format)", "", [] - try: - data = get_json(OSV_VULN + vuln_id) - except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError): - return "(details unavailable)", "", [] - summary = (data.get("summary") - or data.get("details", "")[:160] - or "(no summary)") - severity = "" - for sev in data.get("severity") or []: - score = sev.get("score", "") - match = re.search(r"CVSS:[\d.]+/(\S+)", score) - severity = match.group(1) if match else score - break - fixed = [] - for affected in data.get("affected", []): - for rng in affected.get("ranges", []): - for event in rng.get("events", []): - fix = event.get("fixed") - # Git commit ranges are not actionable version numbers. - if fix and not re.fullmatch(r"[0-9a-f]{40}", fix): - fixed.append(fix) - aliases = [a for a in data.get("aliases", []) if a.startswith("CVE-")] - if aliases: - summary = f"[{', '.join(aliases)}] {summary}" - return summary, severity, sorted(set(fixed)) +def describe(vuln_id, package_name, cache): + """Fetch severity, summary and fixed versions for one advisory.""" + if vuln_id not in cache: + cache[vuln_id] = get_json(OSV_VULN_URL + vuln_id) + vuln = cache[vuln_id] + + fixed = set() + for affected in vuln.get("affected", []): + if affected.get("package", {}).get("name") != package_name: + continue + for entry in affected.get("ranges", []): + for event in entry.get("events", []): + if "fixed" in event: + fixed.add(event["fixed"]) + + return { + "id": vuln_id, + "severity": (vuln.get("database_specific", {}).get("severity") or "UNKNOWN").upper(), + "summary": (vuln.get("summary") or "").strip(), + "fixed": sorted(fixed), + } def main(): - """Scan stdin's coordinates and return a process exit code.""" - coords = read_coords(sys.stdin) - if not coords: - print("::error::no dependency coordinates received - the " - "Gradle report step failed") - return 1 - - print(f"Scanned {len(coords)} resolved dependencies, all modules.\n") - hits = query_osv(coords) - - module_count = len({m for ms in coords.values() for m in ms}) - summary_lines = ["# Dependency security scan", ""] - if not hits: - print("No known vulnerabilities.") - for ga_v, modules in coords.items(): - print(f" ok {ga_v} ({', '.join(modules)})") - summary_lines += [ - f"No known vulnerabilities in **{len(coords)}** resolved " - f"dependencies across {module_count} modules.", - "", - "
Dependencies checked", - "", - "| Dependency | Modules |", - "| --- | --- |", - ] + [ - f"| `{ga_v}` | {', '.join(modules)} |" - for ga_v, modules in coords.items() - ] + ["", "
"] - write_summary(summary_lines) - return 0 + fail_on = os.environ.get("OSV_FAIL_ON", "HIGH").upper() + if fail_on not in SEVERITY_ORDER: + raise SystemExit(f"error: OSV_FAIL_ON must be one of {', '.join(SEVERITY_ORDER)}") - summary_lines += [ - f"**{len(hits)} vulnerable dependency(ies)** out of " - f"{len(coords)} resolved.", - "", - "| Dependency | Modules | Advisory | Severity | Fixed in |", - "| --- | --- | --- | --- | --- |", - ] - for ga_v, vuln_ids in hits.items(): - modules = ", ".join(coords[ga_v]) - print(f"VULNERABLE {ga_v} (declared in: {modules})") - for vuln_id in vuln_ids: - summary, severity, fixed = describe(vuln_id) - fixed_text = ", ".join(fixed) if fixed else "unknown" - print(f" {vuln_id} {severity}") - print(f" {summary}") - print(f" fixed in: {fixed_text}") - print(f" https://osv.dev/vulnerability/{vuln_id}") - # A GitHub annotation so the failure is visible on the PR itself. - print(f"::error title={ga_v} {vuln_id}::{summary} " - f"(fixed in {fixed_text})") - summary_lines.append( - f"| `{ga_v}` | {modules} | " - f"[{vuln_id}](https://osv.dev/vulnerability/{vuln_id})" - f" | {severity or 'n/a'} | {fixed_text} |" - ) - print() - - summary_lines += [ - "", - "Upgrade the dependency in **every** module that declares it - " - "this project declares `org.json` in `sdk-java`, `app-java` " - "and `app-javafx` separately.", - ] - write_summary(summary_lines) - return 1 - - -def write_summary(lines): - """ - Write the report to the job summary and to a file for the PR comment. + blocking_scopes = {s.strip() for s in + os.environ.get("OSV_BLOCKING_SCOPES", "published,sample").split(",") + if s.strip()} + unknown = blocking_scopes - KNOWN_SCOPES + if unknown: + raise SystemExit(f"error: unknown scope(s) in OSV_BLOCKING_SCOPES: {', '.join(sorted(unknown))}") - GITHUB_STEP_SUMMARY only renders on the workflow run page - it does NOT - reach the pull request. The report file is what the PR comment step posts. - """ - body = "\n".join(lines) + "\n" - step_summary = os.environ.get("GITHUB_STEP_SUMMARY") - if step_summary: - with open(step_summary, "a", encoding="utf-8") as handle: - handle.write(body) - report_path = os.environ.get("OSV_REPORT_FILE", "osv-report.md") - with open(report_path, "w", encoding="utf-8") as handle: - handle.write(body) + allowlist = read_allowlist( + os.environ.get("OSV_ALLOWLIST", ".github/dependency-scan-allowlist.txt")) + + entries = read_coordinates(sys.stdin) + if not entries: + raise SystemExit("error: no dependency coordinates on stdin; the Gradle report step " + "produced nothing, so nothing was actually scanned") + + counts = {scope: sum(1 for e in entries.values() if e["scope"] == scope) + for scope in sorted(KNOWN_SCOPES)} + print(f"Scanning {len(entries)} resolved dependencies " + f"({', '.join(f'{n} {s}' for s, n in counts.items())}), " + f"failing on {fail_on}+ in {'/'.join(sorted(blocking_scopes))}") + + hits = query_osv(list(entries)) + + cache = {} + blocking, suppressed, informational = [], [], [] + for coordinate in sorted(hits): + group, name, _ = coordinate.split(":") + for vuln_id in hits[coordinate]: + finding = describe(vuln_id, f"{group}:{name}", cache) + finding["coordinate"] = coordinate + finding["scope"] = entries[coordinate]["scope"] + finding["modules"] = entries[coordinate]["modules"] + entry = allowlist.get(vuln_id) + + can_block = finding["scope"] in blocking_scopes + + if entry and not entry["expired"]: + finding["reason"] = entry["reason"] + finding["expiry"] = entry["expiry"] + suppressed.append(finding) + elif can_block and rank(finding["severity"]) >= rank(fail_on): + finding["expired_allowlist"] = bool(entry) + blocking.append(finding) + else: + informational.append(finding) + + blocking.sort(key=lambda f: (-rank(f["severity"]), f["coordinate"])) + report(entries, blocking, suppressed, informational, fail_on) + return 1 if blocking else 0 + + +def format_finding(finding): + fixed = ", ".join(finding["fixed"]) if finding["fixed"] else "no fixed version published" + return (f"[{finding['scope']}] {finding['coordinate']} — {finding['id']} " + f"[{finding['severity']}]\n" + f" {finding['summary'] or '(no summary)'}\n" + f" fixed in: {fixed}\n" + f" used by: {', '.join(finding['modules'])}") + + +def report(entries, blocking, suppressed, informational, fail_on): + lines = [] + + if blocking: + lines.append(f"BLOCKING — {len(blocking)} vulnerability(ies) at or above {fail_on}:\n") + for finding in blocking: + lines.append(format_finding(finding)) + if finding.get("expired_allowlist"): + lines.append(" note: this advisory's allowlist entry has expired " + "and needs re-review") + lines.append("") + if suppressed: + lines.append(f"ALLOWLISTED — {len(suppressed)}:\n") + for finding in suppressed: + lines.append(f"[{finding['scope']}] {finding['coordinate']} — {finding['id']} " + f"[{finding['severity']}] until {finding['expiry']}: {finding['reason']}") + lines.append("") + if informational: + lines.append(f"INFORMATIONAL — {len(informational)} " + f"(non-blocking scope, or below the {fail_on} threshold):\n") + for finding in informational: + lines.append(format_finding(finding)) + lines.append("") + if not (blocking or suppressed or informational): + lines.append(f"No known vulnerabilities in {len(entries)} resolved dependencies.") + elif not blocking: + lines.append("No blocking findings.") + + text = "\n".join(lines).strip() + print("\n" + text) + + markdown = ("## Dependency security scan\n\n" + f"Scanned **{len(entries)}** resolved dependencies, failing on **{fail_on}** " + f"and above.\n\n```\n{text}\n```\n") + + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if summary_path: + with open(summary_path, "a", encoding="utf-8") as handle: + handle.write(markdown) + + # The job summary only shows on the workflow run page. This file is what the workflow + # posts onto the pull request, where people actually look. + with open(os.environ.get("OSV_REPORT_FILE", "osv-report.md"), "w", encoding="utf-8") as handle: + handle.write(markdown) if __name__ == "__main__": diff --git a/.github/workflows/dependency-security.yml b/.github/workflows/dependency-security.yml index 91296f7e..6bb2b948 100644 --- a/.github/workflows/dependency-security.yml +++ b/.github/workflows/dependency-security.yml @@ -1,7 +1,7 @@ -name: Dependency Security Scan +name: "Dependency Security Scan" -# Runs on pushes/PRs so a bad upgrade is caught immediately, and on a schedule -# because a dependency can become vulnerable without this repo changing at all. +# Runs on pushes/PRs so a bad upgrade is caught immediately, and on a schedule because a +# dependency can become vulnerable without this repo changing at all. on: push: branches: @@ -19,6 +19,13 @@ on: permissions: contents: read +env: + # Modules published to Maven Central. Their runtime dependencies are what every + # integrator inherits, so they are scanned as `published` and always block. + # NOTE: sdk-java only applies the publish plugin when a publish task is requested, so the + # report step cannot auto-detect it -- this list is the only signal. + PUBLISHED_MODULES: ":sdk-java" + jobs: osv-scan: name: OSV scan (all modules) @@ -31,10 +38,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - # NOTE: JDK 17, not the JDK 8 used by gradle.yml. settings.gradle only - # includes :app-javafx on Java 11+, so a JDK 8 run would silently skip - # that module's dependencies -- exactly the blind spot that let a known - # org.json CVE sit in the demo modules after sdk-java had been fixed. + # NOTE: JDK 17, not the JDK 8 used by gradle.yml. settings.gradle only includes + # :app-javafx on Java 11+, so a JDK 8 run would silently skip that module's + # dependencies -- exactly the blind spot that let a known org.json CVE sit in the demo + # modules after sdk-java had been fixed. - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -48,6 +55,7 @@ jobs: run: | set -o pipefail ./gradlew -q --init-script .github/scripts/dependency-report.init.gradle \ + "-DpublishedModules=$PUBLISHED_MODULES" \ printResolvedDependencies | tee resolved-dependencies.txt count=$(grep -c '^COORD' resolved-dependencies.txt || true) echo "Resolved $count coordinate lines." @@ -56,13 +64,19 @@ jobs: exit 1 fi + # Blocks on `published` and `sample` findings -- everything we declare ourselves, + # including the demo modules. `buildscript` is reported only: it is Gradle plugin + # internals, which cannot be upgraded independently of the plugins themselves. - name: Check resolved dependencies against OSV id: osv + env: + OSV_FAIL_ON: HIGH + OSV_BLOCKING_SCOPES: published,sample run: python3 .github/scripts/osv_scan.py < resolved-dependencies.txt - # The job summary written above only shows on the workflow run page. This - # is what actually puts the result on the pull request. It updates one - # sticky comment instead of adding a new one on every push. + # The job summary written above only shows on the workflow run page. This is what + # actually puts the result on the pull request. It updates one sticky comment + # instead of adding a new one on every push. - name: Comment scan result on the pull request if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository uses: actions/github-script@v7 @@ -120,16 +134,16 @@ jobs: path: | resolved-dependencies.txt osv-report.md + if-no-files-found: warn dependency-submission: name: Submit dependency graph - # Runs on pushes to the long-lived branches, on the weekly cron, on manual - # dispatch, AND on same-repo pull requests -- the last one is what gives - # dependency-review a head snapshot to diff against. - # Fork PRs are skipped on purpose: `contents: write` is not granted to a - # workflow triggered by a PR from a public fork, so the submit would fail. - # Covering forks needs the two-workflow generate-and-upload + workflow_run - # pattern from the gradle/actions docs. + # Runs on pushes to the long-lived branches, on the weekly cron, on manual dispatch, + # AND on same-repo pull requests -- the last one is what gives dependency-review a head + # snapshot to diff against. + # Fork PRs are skipped on purpose: `contents: write` is not granted to a workflow + # triggered by a PR from a public fork, so the submit would fail. Covering forks needs + # the two-workflow generate-and-upload + workflow_run pattern from the gradle/actions docs. if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest permissions: @@ -146,12 +160,12 @@ jobs: dependency-review: name: Review dependency changes # Needs the submission job so the head snapshot exists before the diff. - # `always()` keeps this running on fork PRs, where submission is skipped -- - # without it, a skipped dependency would skip this job too. - # SCOPE: this can only diff Gradle dependencies once BOTH the base branch - # and the head have a submitted snapshot, so expect Actions-only output - # until master has been through dependency-submission at least once. - # osv-scan is the job that covers Gradle dependencies unconditionally. + # `always()` keeps this running on fork PRs, where submission is skipped -- without it, + # a skipped dependency-submission would skip this job too. + # SCOPE: this can only diff Gradle dependencies once BOTH the base branch and the head + # have a submitted snapshot, so expect Actions-only output until master has been through + # dependency-submission at least once. osv-scan is the job that covers Gradle + # dependencies unconditionally. if: always() && github.event_name == 'pull_request' needs: dependency-submission runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index c9858cd5..ef03efee 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ data # Gradle build output (all modules, current and future) build/ out/ + +# Dependency security scan output +osv-report.md +resolved-dependencies.txt diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioNetworkDeadlockTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioNetworkDeadlockTests.java index 5a2cd31a..852ec370 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioNetworkDeadlockTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioNetworkDeadlockTests.java @@ -134,7 +134,15 @@ public void serverRecovery_sdkResumesSending() throws Exception { Countly.instance().init(configForLocalServer()); Countly.session().begin(); - Thread.sleep(2000); + + // Poll instead of sleeping a fixed 2s and asserting immediately. The + // property under test is "the SDK does not stay stuck sending", so + // waiting longer for isSending() to clear is correct; a genuine deadlock + // still fails the assertion when the budget runs out. + long deadline = System.currentTimeMillis() + 30_000; + while (SDKCore.instance.networking.isSending() && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + } Assert.assertFalse( "SDK should recover from 502 HTML response", diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java index eaaa4849..9f71340d 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java @@ -309,6 +309,14 @@ public static Map parseQueryParams(String data) { return paramMap; } + /** + * Slack allowed above an expected event duration that was produced by a + * Thread.sleep in a test. Absolute rather than proportional, so it soaks up + * scheduler overrun on a loaded CI runner without meaningfully weakening + * the large, synthetically-set durations some tests assert. + */ + private static final double SLEEP_OVERRUN_TOLERANCE_SECONDS = 2.0; + static void validateEvent(EventImpl gonnaValidate, String key, Map segmentation, int count, Double sum, Double duration, String id, String pvid, String cvid, String peid) { Assert.assertEquals(key, gonnaValidate.key); @@ -324,7 +332,21 @@ static void validateEvent(EventImpl gonnaValidate, String key, Map= duration - delta + && gonnaValidate.duration <= duration + SLEEP_OVERRUN_TOLERANCE_SECONDS); + } } Assert.assertTrue(gonnaValidate.dow >= 0 && gonnaValidate.dow < 7);