pax_global_header00006660000000000000000000000064150675447560014535gustar00rootroot0000000000000052 comment=e79cceb48b3bca7f432ddc34c51e7935d987ae1d appstream-generator-0.10.1/000077500000000000000000000000001506754475600155745ustar00rootroot00000000000000appstream-generator-0.10.1/.clang-format000066400000000000000000000044021506754475600201470ustar00rootroot00000000000000--- BasedOnStyle: LLVM IndentWidth: 4 --- Language: Cpp Standard: C++11 ColumnLimit: 120 BreakBeforeBraces: Linux PointerAlignment: Right AlignAfterOpenBracket: AlwaysBreak AllowAllParametersOfDeclarationOnNextLine: false AlwaysBreakBeforeMultilineStrings: true BreakBeforeBinaryOperators: NonAssignment AlignArrayOfStructures: Left # format C++11 braced lists like function calls Cpp11BracedListStyle: true # do not put a space before C++11 braced lists SpaceBeforeCpp11BracedList: false # no namespace indentation to keep indent level low NamespaceIndentation: None # we use template< without space. SpaceAfterTemplateKeyword: false # Always break after template declaration AlwaysBreakTemplateDeclarations: true # macros for which the opening brace stays attached. ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE ] # keep lambda formatting multi-line if not empty AllowShortLambdasOnASingleLine: Empty # return types should not be on their own lines AlwaysBreakAfterReturnType: None PenaltyReturnTypeOnItsOwnLine: 1000 AlwaysBreakAfterDefinitionReturnType: None # Break constructor initializers before the colon and after the commas, # and never put the all in one line. BreakConstructorInitializers: BeforeColon BreakInheritanceList: BeforeColon PackConstructorInitializers: Never # Place ternary operators after line breaks BreakBeforeTernaryOperators: true # No own indentation level for access modifiers IndentAccessModifiers: false AccessModifierOffset: -4 # Add empty line only when access modifier starts a new logical block. EmptyLineBeforeAccessModifier: LogicalBlock # Only merge empty functions. AllowShortFunctionsOnASingleLine: Empty # Don't indent case labels. IndentCaseLabels: false # No space after C-style cast SpaceAfterCStyleCast: false # Never pack arguments or parameters BinPackArguments: false BinPackParameters: false # Avoid breaking around an assignment operator PenaltyBreakAssignment: 150 # Left-align newline escapes, e.g. in macros AlignEscapedNewlines: Left # Enums should be one entry per line AllowShortEnumsOnASingleLine: false # we want consecutive macros to be aligned AlignConsecutiveMacros: true # never sort includes, only regroup (in rare cases) IncludeBlocks: Regroup SortIncludes: Never appstream-generator-0.10.1/.editorconfig000066400000000000000000000006661506754475600202610ustar00rootroot00000000000000# See https://editorconfig.org/ root = true [*] end_of_line = lf trim_trailing_whitespace = true charset = utf-8 indent_style = space indent_size = 4 [*.d] max_line_length = 120 dfmt_brace_style = knr dfmt_soft_max_line_length = 100 dfmt_align_switch_statements = false dfmt_space_before_function_parameters = true dfmt_keep_line_breaks = true [*.yml] indent_style = space indent_size = 2 [*.xml] indent_style = space indent_size = 2 appstream-generator-0.10.1/.github/000077500000000000000000000000001506754475600171345ustar00rootroot00000000000000appstream-generator-0.10.1/.github/dependabot.yml000066400000000000000000000003161506754475600217640ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "npm" directory: "/" schedule: interval: "monthly" appstream-generator-0.10.1/.github/workflows/000077500000000000000000000000001506754475600211715ustar00rootroot00000000000000appstream-generator-0.10.1/.github/workflows/build-test.yml000066400000000000000000000037301506754475600237730ustar00rootroot00000000000000name: Build Test permissions: contents: read on: [push, pull_request] jobs: build-debian-testing: name: Debian Testing runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Create Build Environment run: cd tests/ci/ && podman build -t asgen -f ./Dockerfile-debian-testing . - name: Build run: podman run -a stdout -a stderr -e CXX=g++ -v `pwd`:/build asgen ./tests/ci/run-build.sh - name: Tests run: podman run -a stdout -a stderr -e CXX=g++ -v `pwd`:/build asgen ./tests/ci/run-tests.sh build-debian-stable: name: Debian Stable runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Create Build Environment run: cd tests/ci/ && podman build -t asgen -f ./Dockerfile-debian-stable . - name: Build run: podman run -a stdout -a stderr -e CXX=g++ -v `pwd`:/build asgen ./tests/ci/run-build.sh - name: Tests run: podman run -a stdout -a stderr -e CXX=g++ -v `pwd`:/build asgen ./tests/ci/run-tests.sh build-fedora-latest: name: Fedora Latest runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Create Build Environment run: cd tests/ci/ && podman build -t asgen -f ./Dockerfile-fedora-latest . - name: Build run: podman run -a stdout -a stderr -e CXX=g++ -v `pwd`:/build asgen ./tests/ci/run-build.sh - name: Tests run: podman run -a stdout -a stderr -e CXX=g++ -v `pwd`:/build asgen ./tests/ci/run-tests.sh build-ubuntu-lts: name: Ubuntu LTS runs-on: ubuntu-24.04 env: CC: gcc-14 CXX: g++-14 steps: - uses: actions/checkout@v5 - name: Create Build Environment run: sudo ./tests/ci/install-deps-deb.sh - name: Make & Install 3rd-party run: sudo ./tests/ci/ci-install-extern.sh - name: Build run: ./tests/ci/run-build.sh - name: Tests run: ./tests/ci/run-tests.sh appstream-generator-0.10.1/.github/workflows/codeql.yml000066400000000000000000000014121506754475600231610ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ master ] pull_request: branches: [ master ] schedule: - cron: '36 8 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-24.04 env: CC: gcc-14 CXX: g++-14 strategy: fail-fast: false steps: - name: Checkout repository uses: actions/checkout@v5 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: cpp - name: Create Build Environment run: sudo ./tests/ci/install-deps-deb.sh - name: Make & Install 3rd-party run: sudo ./tests/ci/ci-install-extern.sh - name: Build & Test run: ./tests/ci/run-build.sh codeql - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 appstream-generator-0.10.1/.github/workflows/coverity.yml000066400000000000000000000026751506754475600235720ustar00rootroot00000000000000name: Coverity Scan permissions: contents: read on: workflow_dispatch: schedule: - cron: '0 3 1,15 * *' jobs: coverity: if: github.repository == 'ximion/appstream-generator' runs-on: ubuntu-24.04 env: CC: gcc-14 CXX: g++-14 steps: - uses: actions/checkout@v5 with: fetch-tags: true fetch-depth: 0 - name: Create Build Environment run: sudo ./tests/ci/install-deps-deb.sh - name: Make & Install 3rd-party run: sudo ./tests/ci/ci-install-extern.sh - name: Create Version String id: version run: | git_commit=$(git rev-parse --short HEAD) git_current_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo v1.0.0) git_commit_no=$(git rev-list --count "${git_current_tag}..HEAD" 2>/dev/null) git_version_full=${git_current_tag#v}; git_version_full=${git_version_full//-/.} if [ "$git_commit_no" -gt 0 ]; then git_version_full+=".git$git_commit_no" fi echo "Using version string: $git_version_full" echo "git_version_full=$git_version_full" >> $GITHUB_OUTPUT - name: Coverity uses: vapier/coverity-scan-action@346291b388d0a99b2d82c8d46f5088d0fc494844 with: email: ${{ secrets.COVERITY_SCAN_EMAIL }} token: ${{ secrets.COVERITY_SCAN_TOKEN }} version: ${{ steps.version.outputs.git_version_full }} command: './tests/ci/run-build.sh coverity' appstream-generator-0.10.1/.gitignore000066400000000000000000000001121506754475600175560ustar00rootroot00000000000000build/ .dub docs.json dub.selections.json __dummy.html *.o *.obj *.so *.a appstream-generator-0.10.1/LICENSE000066400000000000000000000167431506754475600166140ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. appstream-generator-0.10.1/MAINTAINERS000066400000000000000000000000471506754475600172720ustar00rootroot00000000000000Matthias Klumpp E-mail: mak@debian.org appstream-generator-0.10.1/NEWS000066400000000000000000000777431506754475600163150ustar00rootroot00000000000000Version 0.10.1 ~~~~~~~~~~~~~~ Released: 2025-1ß-02 Features: * engine: Limit default parallelism for main processing operations * Skip intermediate data check if gcid is empty * Print if data remains after manually removing a package * engine: Add function to process all suitable data at once Bugfixes: * tests: Allow network test to have all tests skipped * utils: Just use C++ standard library for copying directories * Guard against null values in the database * zarchive: Properly normalize paths * yaml: Make sure Time field is always read as string * Explicitly set up UTF-8 locale * Ensure we aren't running on a bad libfyaml * engine: Actually open the locale unit, so it can be used to find locale * Don't just skip processing most new packages Miscellaneous: * Update the snapcraft file Contributors: Matthias Klumpp Version 0.10.0 ~~~~~~~~~~~~~~ Released: 2025-09-16 Notes: * This release switches from using the D programming language to C++23. This change should simplify maintenance of the tool and allow it to be more compatible with other tooling. C++23 adopted a lot of D's features, so this is a pretty direct port - still, issues may exist even though this release has been tested extensively. So, perform your own tests and report any issues! Features: * Translate the project from D to C++23 * zarchive: Support reading from hardlinks * Use backward-cpp if available for automated crash backtraces * Add optimization for repeated reads from large archives * Add support for loading JPEG-XL icons Bugfixes: * zarchive: Ensure parent path is created before opening file * debian: Ensure legacy-style repos get package descriptions for components * Create document timestamps in the right format * iconhandler: Render SVG with new API and properly report errors again Miscellaneous: * Add and enforce an npm lockfile * Avoid some unnecessary data copies and refcounting where possible Contributors: Joey Riches, LN Liberda, Matthias Klumpp Version 0.9.2 ~~~~~~~~~~~~~~ Released: 2025-08-12 Features: * data: Add tooltip hover for plot graph in section_pages.html Bugfixes: * snap: Don't include our glib * freebsd backend: Start from meta.conf when gathering repository metadata * freebsd: catch up with new pkg repository layout * rpmmd: Port to dxml * rpmmd: Download packages on-demand * Sanitize UTF-8 before adding it to the database * Drop removed no-bin-links argument from yarn invocation Miscellaneous: * Add AdwaitaLegacy to themeNames * snap: Update some dependency versions * snap: Build using system LDC and glibd * Install JS stuff with NPM * metainfo: Add developer name Contributors: Amin Bandali, Balló György, Gleb Popov, Joey Riches, Matthias Klumpp Version 0.9.1 ~~~~~~~~~~~~~~ Released: 2023-11-11 Features: * Implement a FreeBSD backend * Add I/O caching for the FreeBSD backend * Allow to manually specify a GIR directory at build time * Include Yarn lockfile * Add release information to metainfo file * Build against AppStream 1.0 Bugfixes: * Make building the RPMMD backend optional * web: Better split archive sections * ubuntu: Don't fail on repositories with no language packs * utils: mkdirRecurse instead of mkdir inside parallel Miscellaneous: * Snap updates * Add documentation how to install from Flathub * Autoformat source code Contributors: Gleb Popov, Heather Ellsworth, Matthias Klumpp, Pablo Correa Gómez Version 0.9.0 ~~~~~~~~~~~~~~ Released: 2023-01-26 Features: * Allow more complex repo-level metadata additions Documentation: * docs: Document modifications.json * docs: document values of "Backend" in asgen config * docs: clarify directory structure Bugfixes: * Make MetadataType override work again * Ensure all user feature flags are applied properly again * Retry failed download 4 times by default * Even process an empty repository if action is forced * Don't fail if icon directory for injected metadata is missing * ci: Improve scripts and don't use deprecated commands * ci: Install ffmpeg in the Fedora images Contributors: Matthias Klumpp, Miroslav Suchý, Neal Gompa Version 0.8.8 ~~~~~~~~~~~~~~ Released: 2022-04-10 Bugfixes: * Use higher stack size for archive read generator Contributors: Matthias Klumpp Version 0.8.7 ~~~~~~~~~~~~~~ Released: 2022-02-22 Bugfixes: * Fix build on 32-bit architectures * Set CAInfo on compose instance as well * Don't unlink components of fake packages * Process icons by suite/section across all architectures * Fix a bunch of Meson and D deprecation warnings Version 0.8.6 ~~~~~~~~~~~~~~ Released: 2021-12-22 Notes: * This is the first release to fully use libappstream-compose for almost all metadata analysis (except icon processing, which needs special care). Features: * Reimplement data extractor using the compose API * Enable screencasts in compose based on our screenshotVideos feature flag * Run external metadata through the exact same processing as all other data * Redirect glib debug messages to our own logging if in verbose mode * Adjust parallel processing work unit sizes for seeding * ci: Add Fedora to the CI environment Bugfixes: * Fix build with GLib >= 2.69 * engine: Protect iconTarFiles variable when scanning for icons in parallel * Resolve some issue tag conflicts with as-compose * Control batch processing better to enable better extractor reuse * Add duplicate-metainfo exemption for injected data * Improve explanation of the "no-metainfo" tag * Don't emit "description-from-package" if "no-metainfo" was already present Contributors: Neal Gompa (ニール・ゴンパ), Matthias Klumpp Version 0.8.5 ~~~~~~~~~~~~~~ Released: 2021-08-28 Features: * snap: Set `grade` to `stable` * Improve metainfo/desktop deduplication code * Use ascompose for adding translation status data Bugfixes: * Always mark desktop-file as handled when handling its metainfo file (#91) * Fix compilation due to broken generated GdkPixbuf code * config: Move setUseOptipng call after we determine if optipng is available * snap: Add glib{,-networking} to asgen's stage-packages * snap: Build libas from a tag * Adjust for compose API changes Contributors: Balló György, Dan Printzell, Iain Lane and Matthias Klumpp Version 0.8.4 ~~~~~~~~~~~~~~ Released: 2021-03-02 Features: * Replace our HashMap with native associative arrays * Switch back to using the curl-based downloader again * Use the desktop-entry parsing code from appstream-compose * Try finding icons in /usr/share/icons root directory as well * Update our own metainfo file and validate it Bugfixes: * snap: Set LD_LIBRARY_PATH to the stage dir for asgen's build * snap: Install curl as a normal build-package * snap: Build with -j1 * debian: Forget a package's filename when removing its temporary dir * debian: Add more synchronization * zarchive: Log the error when we can't open an archive * various: Improve builddir != srcdir builds and build the snap like this * Replace theme icon generator concurrency with a range * snap: Build AppStream in debugoptimized mode instead of release mode Contributors: Iain Lane, Matthias Klumpp Version 0.8.3 ~~~~~~~~~~~~~~ Released: 2021-02-02 Notes: * This release requires libappstream-compose from the AppStream project to build. The library is still in progress and currently has an unstable API, but is developed in sync with appstream-generator, so asgen can safely depend on it. Features: * Elevate no-metainfo presence to a warning by default (Matthias Klumpp) * Ignore all apps with only desktop-entry files using OnlyShowIn (Matthias Klumpp) * Make use of the helper classes provided by appstream-compose (Matthias Klumpp) * Add editorconfig (Matthias Klumpp) * Use ascompose result type as base for the generator result container (Matthias Klumpp) * Use the validator helper function from appstream-compose (Matthias Klumpp) * Use metainfo parsing helper from appstream-compose (Matthias Klumpp) * alpine: add capabilities to download packages via HTTP (Rasmus Thomsen) * config: allow overriding export dir via --export-dir (Rasmus Thomsen) Bugfixes: * Add explicit option to disable network-dependent tests (Matthias Klumpp) * Add missing CSS styling for permalinks to Debian template (Matthias Klumpp) * Captialize "MetaInfo" the same everywhere in hint messages (Matthias Klumpp) * Use binding generator to create the missing AppStream Utils functions (Matthias Klumpp) * Never open contents cache DB env more than once in the same thread (Matthias Klumpp) * Re-enable LMDB TLS support (Matthias Klumpp) * downloader: Check read byte count before appending to buffer (Matthias Klumpp) * Ensure export directory paths are sane, absolute paths all the time (Matthias Klumpp) Version 0.8.2 ~~~~~~~~~~~~~~ Released: 2020-05-12 Features: * Add experimental Snapcraft build definition * snap: Move to strict confinement (Iain Lane) * snap: Lots of debugging & integration work (Iain Lane) * Find external binaries in PATH instead of hardcoding absolute paths to them * Add static bindings for libsoup * Switch to new libsoup-based downloader, drop Curl * Always log the generator version for some operations * Display more verbose debug messages when downloads are retried * Better messages for network connection errors * Support Ubuntu language packs for l10n status info extraction * Make permalink anchors visible in HTML output * alpine: Add new backend for Alpine Linux (Rasmus Thomsen) Bugfixes: * Pull a few Mustache engine code improvements from upstream * Throw a better error message in unit tests when ffprobe isn't found * tests: Skip video metadata check if ffprobe can not be found * Check if our GdkPixbuf is able to handle all essential image formats * Slighty reduce default logging verbosity when finding icons * Give up icon search when component was rejected in icon storage routine * Swallow bad last-modified times from servers instead of freaking out * Make downloader a thread-local singleton (instead of thread-global) * Create XDG runtime dir in case it is missing * Make a failure to read an archive symlink target non-fatal * Print available font names in issue report if no matching font was found * Handle unexpected NULL-byte files better * Find Yarn, even when it was renamed * Strip out release artifacts for components that have a package Version 0.8.1 ~~~~~~~~~~~~~~ Released: 2020-01-20 Bugfixes: * Don't crash if a gettext locale has no strings * Reject packages which should have a long description but don't have one * debian: Search for bz2 files again, i18n still uses those * Work around some curl/D behavior changes and make HTTPS downloads work again * Check suite status before removing hints/components * Don't fail download on HTTP status code 302 * Properly fail icon search in all failure cases, add more debug logging * Ensure icon tarballs are closed immediately after writing to them * Flip around sorting of suites on HTML reports Version 0.8.0 ~~~~~~~~~~~~~~ Released: 2019-09-24 Notes: * This release needs FFmpeg installed if video screenshots should be permitted. No transcoding will be done. Check out the options to modify this feature in the documentation. Features: * Add Igor Khasilev's container implementations to the source * Replace EMSI containers with built-in implementation * Bump dependency on AppStream * Fix build with latest AppStream * Implement improved validation issue reporting * Always build registry of all hint tags, instead of adding them on-demand * Add AV1 video sample for video support tests * Add video tag support, analyze and store videos * Use generic download method for downloading screenshots as well Bugfixes: * Make dscanner happy again * Drop wrap files: We don't need them anymore * debian: Don't check for bz2 compressed indices * Only add remote icons if we have a mediaBaseUrl and are permitting image caches * Validate injected metainfo files as well * Ignore pedantic issues in HTML reports * Fix getFileContents when acting on remote data * XML-escape validator explanations Version 0.7.7 ~~~~~~~~~~~~~~ Released: 2019-02-24 Features: * Speed up locale search by moving it to its own database cache (Matthias Klumpp) Bugfixes: * Component removal requests don't need to have a package-name set (Matthias Klumpp) Version 0.7.6 ~~~~~~~~~~~~~~ Released: 2019-01-10 Features: * Limit the amount of release metadata in output (Matthias Klumpp) * Implement icon loading for injected metadata (Matthias Klumpp) * Prefer some SFNT font metadata over internal heuristics (Matthias Klumpp) * Implement Gettext locale processing (Matthias Klumpp) * Use a struct instead of bitfield to store enabled generator features (Matthias Klumpp) * Refactor locale statistics loader (Matthias Klumpp) * Explicitly remove temporary package data after reading locale info (Matthias Klumpp) * Make arch-specific injected metadata override arch:all data (Matthias Klumpp) Bugfixes: * Complain if the user wants to associate a component with a fake package (Matthias Klumpp) * Check that fonts and OS components have an icon (Matthias Klumpp) * Sanitize image URLs before attempting download (Matthias Klumpp) Version 0.7.5 ~~~~~~~~~~~~~~ Released: 2019-01-04 Features: * Load components for Arch Linux (Balló György) * Embed mustache-d code (Matthias Klumpp) * Protect against HTTPS-to-HTTP downgrades (Matthias Klumpp) * Allow asgen maintainer to mark components for removal and inject additional metainfo files (Matthias Klumpp) * docs: Document the metainfo injection feature (Matthias Klumpp) Bugfixes: * Fix a few issues found by dscanner (Matthias Klumpp) * Properly error out on HTTP 404 status codes (Matthias Klumpp) * Add wrapfile for mir-core (Matthias Klumpp) * Also ignore web applications without an icon (Matthias Klumpp) Version 0.7.4 ~~~~~~~~~~~~~~ Released: 2018-08-04 Features: * Write CID<->GCID mapping table as additional output data (Matthias Klumpp) * Don't rebuild GLibD as part of asgen, use the shared library instead (Matthias Klumpp) * Add hint to add launchable tag if .desktop file is missing (Matthias Klumpp) * Use posix_spawn codepath for optipng if possible via GLib (Matthias Klumpp) * Unconditionally add stock-type icon if desktop-file allows for it (Matthias Klumpp) * Allow font languages to be specified in a languages tag (Matthias Klumpp) * Improve font language processing with metainfo hints (Matthias Klumpp) * fonts: Sort selected font styles, prefer regular style for samples (Matthias Klumpp) * fonts: Use a random pangram for fonts supporting English (Matthias Klumpp) * fonts: Assume 100% language support for all locale mentioned in font data (Matthias Klumpp) Bugfixes: * Fix build with phobos 2.081 (Antonio Rojas) * Depend on non-broken Meson version (Matthias Klumpp) * fonts: Always prefer English for samples if font supports it (Matthias Klumpp) * Initialize font icon lookup table only if there are fonts (Matthias Klumpp) Version 0.7.3 ~~~~~~~~~~~~~~ Released: 2018-04-26 Features: * Don't include desktop files with an empty OnlyShowIn and complain about that * Improve decompression code slightly * Improve code to check for inclusion of .desktop files * Optimize category filter function * ubuntu: Use GC managed arrays for langpack info storage * ubuntu: Hold copy of language pack array per package * ubuntu: Don't load language packs multiple times, cleanup index data properly * Close package from a base suite immediately in the seeding step Bugfixes: * debian: Make package index threadsafe again * Don't add data to contents store in parallel * Don't add the same architecture multiple times when generating reports * Enable static check for poor exception handling * Don't run through a synchronized section when fetching package filename * Update dscanner and invoke it from a more versatile Python script Version 0.7.2 ~~~~~~~~~~~~~~ Released: 2018-04-16 Features: * Dramatically reduce memory usage of cruft cleanup operation * Parallelize database cleanup a bit more * debian: Add basic in-memory deduplication of translated texts for packages Bugfixes: * Fix build on 32bit architectures * Make GStreamer entry entirely optional for packages * Ensure we properly merge in desktop-entry data from launchable tags * Never process desktop files twice * Don't append desktop data to mime/category lists * Update wrap files to work with gir-to-d >= 0.15 Version 0.7.1 ~~~~~~~~~~~~~~ Released: 2018-04-06 Features: * debian: Use TagFile class from Laniakea * debian: Use the packages real architecture in its identifier * debian: Implement packageForFile backend feature * Do more complex parsing of metainfo license expressions * ci: Enable more static analysis checks * Improve manual page Bugfixes: * Never upscale icons if we don't need to, use pixmaps directory as last resort * Update wrap files to work with gir-to-d >= 0.14 * Don't try to upscale tiny icons from the pixmaps directory * Register HiDPI icons for fonts properly * Fix some minor style issues found by D-Scanner * Document WorkspaceDir config file option Version 0.7.0 ~~~~~~~~~~~~~~ Released: 2018-04-04 Notes: * Processing single packages and calling publication and processing separately is not a fully supported feature yet! This release lays the groundwork for it, but its final implementation will happen later. Do not use this in production! Features: * stdx.allocator-backed containers library * Use stdx.allocator-backed HashMap in more places * Trigger GC less aggressively, use smaller parallel work unit sizes * Allow setting a workspace in config and calling asgen on a config file * Add backend interface for processing a single package * Allow calling only the metadata publication step * Add framework for extracting data directly from package files * Allow setting icon policy in configuration * Add support for more icon types and customizable icon policy * Allow icon upscaling in certain cases, but complain about it * Modernize Meson definitions * Document how to modify icon policies in configuration * Use AppStream 0.12.0 and format 0.12 by default Bugfixes: * Update wrap files to be compatible with gir-to-d >= 0.13 * Improve ArchiveDecompressor usage * For each array appender, try to guess capacity to reserve better * Properly add remote icon URLs * Fetch JavaScript bits with Yarn instead of Bower * Ensure we can run asgen again from its build directory * Ignore data from immutable suites on cleanup * Don't parallelize cleanup data collection routine * Release memory chunks faster during cleanup operations Version 0.6.8 ~~~~~~~~~~~~~~ Released: 2017-11-06 Notes: * The -Ddownload_js build flag is now -Ddownload-js to follow the naming of other Meson flags more closely. Features: * Encode AppStream library version in version info string on reports * Don't require an install candidate for a webapp * Update hicolor theme fallback definition * Try to filter out symbolic icons for apps * Allow processing only one section in a suite * Relax icon scaling rules Bugfixes: * Move time graph legend to the top left * ci: Use the packaged gir-to-d * Drop an unused gdc conditional * Ensure the validator never tests web URLs for validity * Drop LLVM DC bug workaround and allow cross-module inlining * Make D GIR interface build work again with recent Meson versions * Don't fail if we have a dupe ID with no pkgname, can happen now with webapps Version 0.6.7 ~~~~~~~~~~~~~~ Released: 2017-10-02 Features: * ci: Enable gdc again for testing (Matthias Klumpp) * Make rpmmd backend work (Matthias Klumpp) * Make an empty main function for embedded unittests (Matthias Klumpp) * Add a metainfo file (Matthias Klumpp) * Add a manual page (Matthias Klumpp) * Sort suite names on the index page (Matthias Klumpp) Bugfixes: * use file.exists to check file existence (Antonio Rojas) * arch: Don't ref generator results (Matthias Klumpp) * Improve the icon tarball generation code (Matthias Klumpp) * Resolve deprecation warning (Matthias Klumpp) Version 0.6.6 ~~~~~~~~~~~~~~ Released: 2017-09-22 Features: * Add basic HiDPI support (Corentin Noël) * Generate the HiDPI tarball (Corentin Noël) * Add an 'info' command to dump package information (Matthias Klumpp) * Handle the "Hidden" property in .desktop files and complain about it (Matthias Klumpp) Bugfixes: * arch: XML-escape package descriptions (Matthias Klumpp) * Check for availability of a component-id at the right time (Matthias Klumpp) * Make writing compressed results a bit more robust (Matthias Klumpp) * Resolve all deprecation warnings and slightly improve code (Matthias Klumpp) Version 0.6.5 ~~~~~~~~~~~~~~ Released: 2017-07-02 Features: * Generate type=codec metadata for gstreamer packages (Iain Lane) * Add a feature flag for GStreamer processing (Iain Lane) * ci: Switch to using upstream GirToD (Matthias Klumpp) * Set asgen version in defines.d by Meson as well (Matthias Klumpp) * Format graphs a bit nicer (Matthias Klumpp) Bugfixes: * Find files in DATADIR - not relative to the executable (Iain Lane) * Fix typo in metadata-path hint (Matthias Klumpp) * Try to be more informative when a component has no ID (Matthias Klumpp) * Ensure the rDNS scheme is really followed before editing an ID (Matthias Klumpp) Version 0.6.4 ~~~~~~~~~~~~~~ Released: 2017-05-25 Features: * Automatically generate GIR D bindings at build-time (Matthias Klumpp) * Remove dub build file (Matthias Klumpp) * Use Meson subproject/wrap to fetch mustache-d in case it is missing (Matthias Klumpp) * Add a launchable tag to output if we can add one (Matthias Klumpp) * Make legacy metainfo dir a warning (Matthias Klumpp) * Validate launchable entries and add heuristics in case they aren't present (Matthias Klumpp) Bugfixes: * Import buildPath directly from std.path (Antonio Rojas) * Remove spurious std.stream import (Matthias Klumpp) Version 0.6.3 ~~~~~~~~~~~~~~ Released: 2017-03-08 Features: * Experiment with scoped classes (Matthias Klumpp) * Drop embedded generator copy (Matthias Klumpp) * Use final classes whenever possible (Matthias Klumpp) * Centralize all export-dir locations in base config class (Matthias Klumpp) * Allow to manually configure data export locations (Matthias Klumpp) Bugfixes: * Fix some quirks and make the tests work again (Matthias Klumpp) * ci: Don't build with GDC (Matthias Klumpp) * Don't fail if a suite has no base suite (Matthias Klumpp) * engine: Process base suite packages for contents only (Iain Lane) * Update README.md (#39) (Blake Kostner) * debian: Only select the most recent packages for a scan (Matthias Klumpp) * Streamline final component validity check, resolve a crash (Matthias Klumpp) Version 0.6.2 ~~~~~~~~~~~~~~ Released: 2017-01-24 Notes: * GDC 6.x can't compile the project at time due to a GDC bug. See https://bugzilla.gdcproject.org/show_bug.cgi?id=251 for more information. Features: * Support desktop-app metainfo files without .desktop file if they have an icon set * Write log entry when starting/finishing icon tarball write * Update AppStream bindings * Allow to specify allowed keys * Don't show pedantic validator hints * Allow metainfo file to specify sample texts for fonts * Process XPM icons if they are large enough Bugfixes: * Safeguard against TLD checks with empty-or-null string * Ensure desktop-apps have at least one valid category set * Don't override metainfo name/summary with .desktop values * Quit immediately if the component type is unknown * Correctly read a font's full-name * Don't needlessly reprocess fonts Version 0.6.1 ~~~~~~~~~~~~~~ Released: 2016-12-26 Features: * Add a symlink so that Ubuntu uses the same template as Debian (Iain Lane) * Support linking to "old suites" in the HTML index (Iain Lane) * hints: icon-not-found: Explain the symlink problem for Debian & Ubuntu (Iain Lane) * Add preliminary new asgen logo (Matthias Klumpp) * Use struct for archive (Matthias Klumpp) * Reuse AsMetadata while processing metainfo files (Matthias Klumpp) Bugfixes: * Correctly install templates with Meson (Matthias Klumpp) * meson: Make finding mustache-d more robust (Matthias Klumpp) * Fix build with DMD (Antonio Rojas) * engine: Don't clean packages in base suites (Iain Lane) * download: If we have a last-modified date, set the mtime of the target file (Iain Lane) * Simplify zarchive code a little (Matthias Klumpp) * Use module initializer to initialize global static data (Matthias Klumpp) * Fix build on Debian with GDC (Matthias Klumpp) Version 0.6.0 ~~~~~~~~~~~~~~ Released: 2016-10-03 Notes: * If possible, you should build Meson with the LLVM D compiler and the Meson build system now. Features: * Produce better error messages on failed libarchive actions (Matthias Klumpp) * Add a hook to the desktop file parser to run backend specific code (Iain Lane) * Add an Ubuntu backend to retrieve langpack translations (Iain Lane) * Open databases with NOTLS (Matthias Klumpp) * Tweak the issue messages a bit (Matthias Klumpp) * Complain about stuff in legacy paths (Matthias Klumpp) * Only search for .desktop files if we have a DESKTOP_APP component (Matthias Klumpp) * Create new Font class to read font metadata (Matthias Klumpp) * Render an icon for fonts (Matthias Klumpp) * Add dependencies on Pango, FreeType and Fontconfig (Matthias Klumpp) * Determine languages a font supports (Matthias Klumpp) * Reorganize code for proper namespacing (Matthias Klumpp) * Render font screenshots (Matthias Klumpp) * Map font full names to files in a spec-compliant way (Matthias Klumpp) * Enable fonts support by default (Matthias Klumpp) * Share ContentsStore between threads again (Matthias Klumpp) * Make Meson a first-class buildsystem for asgen (Matthias Klumpp) * Update README (Matthias Klumpp) Bugfixes: * Fix build on non-64-bit architectures (Matthias Klumpp) * Work around LDC bug (Matthias Klumpp) * fclose() the memstream when we are done with it (Iain Lane) * debpkgindex: Be less noisy about translations in debug mode (Iain Lane) * Use getTestSamplesDir (Iain Lane) * Retry on all curl errors, not just timeouts (Iain Lane) * Synchronise downloading of files (Iain Lane) * ubuntu: Don't reference all packages, just the ones we need (langpacks) (Iain Lane) * Use an ugly mutex to work around Fontconfig issues (Matthias Klumpp) * Make FC mutex a bit more fine-grained (Matthias Klumpp) Version 0.5.0 ~~~~~~~~~~~~~~ Released: 2016-08-30 Features: * Add more speed and style optimizations, make Package an abstract class (Matthias Klumpp) * Use a string appender instead of a string array for writing output (Matthias Klumpp) * debian: Support downloading files from a mirror, instead of having them local (Iain Lane) * debian: Support DDTP translations (Iain Lane) * Ignore some more useless categories (Matthias Klumpp) * Add skeleton for RPMMD (Matthias Klumpp) * Add a few optimizations using immutable and appender (Matthias Klumpp) * Do not spawn compressors anymore, use libarchive directly (Matthias Klumpp) * Make compiling with LDC easier (Matthias Klumpp) * Add example for cruft collection script (Matthias Klumpp) * Allow cleaning up statistical data retroactively (Matthias Klumpp) * Rehash hash tables that we query often (Matthias Klumpp) * Always optimize debug builds (Matthias Klumpp) * Make a splitbuild with Meson and Ninja possible (Matthias Klumpp) * Use AppStream's knowledge about TLDs to build smarter global-component-ids (Matthias Klumpp) * Allow specifying the format version metadata should be built for (Matthias Klumpp) * Adjust for AppStream 0.10 release (Matthias Klumpp) * Perform legal checks before allowing metadata to be added to the pool (Matthias Klumpp) Bugfixes: * Fix dataUseJSTime in section overview template (Blake Kostner) * debian: Only retrieve DDTP translations from the section we're looking at (Iain Lane) * Fix deprecation warnings from LDC (Matthias Klumpp) * Work around some weird JSON parser quirk (int incompatible with uint) (Matthias Klumpp) * Prevent creating excess statistic entries (Matthias Klumpp) * Fix potential crash (Matthias Klumpp) * Use native generator when not compiling with GDC (Matthias Klumpp) * Work correctly if CIDs omit the .desktop suffix (Matthias Klumpp) * Only add valid category names to output (Matthias Klumpp) * Don't emit description-from-package multiple times per package. (Matthias Klumpp) Version 0.4.0 ~~~~~~~~~~~~~~ Released: 2016-07-12 Features: * README: Add link to D tour * Run optipng with standard settings * Add command to forget extracted data for a package * Implement immutable suites * Use appender more often and apply some style fixes * Build with parallel by default when using Makefile * Drop some deprecated stuff Bugfixes: * Display an error when processing a suite without section or arch * debian: Fix exception message if directory does not exist * Code improvements: More pure, more safe, more trusted * Drop global data validation result box * Don't crash if media pool directory does not exist and immutable suites are used Version 0.3.0 ~~~~~~~~~~~~~~ Released: 2016-05-24 Features: * Register new backend for Arch Linux (Matthias Klumpp) * arch: Add reader for lists index file and skeleton for PackageIndex (Matthias Klumpp) * Make generic archive class work well with non-Debian packages (Matthias Klumpp) * Add HTML anchors for maintainers on report pages (Matthias Klumpp) * html: Show suite name in section overview (Matthias Klumpp) * Allow not setting a MediaBaseUrl (Matthias Klumpp) * Add some font rendering experiments (Matthias Klumpp) * Allow storing multiple statitics entries per point in time (Matthias Klumpp) * Make icon tarball build reproducible (Matthias Klumpp) * Make ArchiveDecompressor API more powerful and always return const(ubyte)[] (Matthias Klumpp) * arch: Speed up backend by loading all data in one go (Matthias Klumpp) * Split "handleScreenshots" into more useful, fine-grained flags (Matthias Klumpp) * Add timestamp to output and only touch it if something has changed (Matthias Klumpp) * Only update metadata if the indices have been changed (Matthias Klumpp) * Store media in pool subdirectory (Matthias Klumpp) * Add flag to enforce metadata processing, even if nothing changed (Matthias Klumpp) * Allow disabling metadata timestamps (Matthias Klumpp) * Improve scan-skipping code (Matthias Klumpp) Bugfixes: * Fix build on non-64bit arches (Matthias Klumpp) * Handle compressed empty files correctly (Neil Mayhew) * Drop other, non-UTF-8 encodings from language codes too (Matthias Klumpp) * Drop non-printable characters from .desktop file values (Matthias Klumpp) * Loop over more things by-reference to reduce RAM usage (Matthias Klumpp) * debian: Read Packages.xz files if no .gz file is available (Matthias Klumpp) * Don't lie about thumbnail sizes in their filename (Matthias Klumpp) * Don't duplicate architecture tag if there are multiple issues (Matthias Klumpp) * Catch data serialization errors (Matthias Klumpp) * Run bower with allow root to be able to make js as root (Harald Sitter) Version 0.2.0 ~~~~~~~~~~~~~~ Released: 2016-04-24 Features: * Don't ship minified JS * Get rid of (almost) all the embedded JS copies, use Bower * Add makefile for convenience * Use Flot for drawing graphs * Update README * Document all asgen-config settings * Reserve subdb in contents database to cache icon data * Make IconHandler use a pre-filtered list of icons * Allow templates to override only parts of the default branding * Add some default branding for Debian * Show logo on generated HTML pages * debian: Link some interesting resources from the main page Bugfixes: * Find aliased icons correctly * Keep priority sorting of themes * Make the hicolor theme always-available, using an embedded index copy if necessary * Fix counting of issues * Do not accidentally upscale screenshots while creating thumbnails * debian: Don't make a missing package index fatal * Don't fail if we are trying to add statistics too quickly * Rename remove-valid to reflect what it actually does * Ignore errors if we are trying to decompress a 0-byte gzip file * Suite arguments aren't optional for process/remove-found * Demote screenshot-no-thumbnails to info Version 0.1.0 ~~~~~~~~~~~~~~ Released: 2016-04-18 Notes: * Initial release appstream-generator-0.10.1/README.md000066400000000000000000000076251506754475600170650ustar00rootroot00000000000000# AppStream Generator AppStream is an effort to provide additional metadata and unique IDs for all software available in a Linux system. This repository contains the server-side of the AppStream infrastructure, a tool to generate metadata from distribution packages. You can find out more about AppStream collection metadata at [Freedesktop](https://www.freedesktop.org/software/appstream/docs/chap-CollectionData.html). The AppStream generator is currently primarily used by Debian, but is written in a distribution agnostic way. Backends only need to implement [two interfaces](src/backends/interfaces.h) to be ready. If you are looking for the AppStream client-tools, the [AppStream repository](https://github.com/ximion/appstream) is where you want to go. ## Install from Flathub You can install an up-to-date version of AppStream Generator from [Flathub](https://flathub.org) if you just want to quickly test the software with your repository: ```ShellSession # Add Flathub remote sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo # Install appstream-generator flatpak install org.freedesktop.appstream.generator # Run appstream-generator flatpak run org.freedesktop.appstream.generator --help ``` You can use AppStream Generator as [documented](docs/usage.md), but you will need to replace all `appstream-generator` commands with `flatpak run org.freedesktop.appstream.generator` and may need to set the workspace as absolute path using `-w` instead of relying on autodetection. ## Usage Take a look at the [docs/](docs/index.md) directory in the source tree for information on how to use the generator and write configuration files for it. ## Development ![Build Test](https://github.com/ximion/appstream-generator/workflows/Build%20Test/badge.svg) ### Build dependencies This project requires a C++23-capable compiler, GCC >= 14 or Clang >= 18 is recommended. The following libraries and tools are required to build the generator: * Meson (>= 1.0) [1] * AppStream [2] * libarchive (>= 3.2) [3] * LMDB [4] * Curl * Cairo * GdkPixbuf 2.0 * RSvg 2.0 * FreeType * Fontconfig * Pango * Inja [5] * Catch2 [6] * oneAPI TBB [7] * NPM (optional) [8] [1]: http://mesonbuild.com/ [2]: https://github.com/ximion/appstream [3]: https://libarchive.org/ [4]: https://symas.com/lmdb/ [5]: https://github.com/pantor/inja [6]: https://github.com/catchorg/Catch2 [7]: https://uxlfoundation.github.io/oneTBB/ [8]: https://github.com/npm/cli On Debian and derivatives of it, all build requirements can be installed using the following command: ```ShellSession sudo apt install meson g++ \ libappstream-dev libappstream-compose-dev libsoup2.4-dev libarchive-dev \ libgdk-pixbuf2.0-dev librsvg2-dev libcairo2-dev libfreetype-dev libfontconfig1-dev \ libpango1.0-dev liblmdb-dev libtbb-dev libcatch2-dev \ npm ``` ### Build instructions To build the tool with Meson, create a `build` subdirectory, change into it and run `meson .. && ninja` to build. In summary: ```ShellSession $ mkdir build && cd build $ meson -Ddownload-js=true .. $ ninja $ sudo ninja install ``` We support several options to be set to influence the build. Change into the build directory and run `mesonconf` to see them all. You might want to perform an optimized debug build by passing `--buildtype=debugoptimized` to `meson` or just do a release build straight away with `--buildtype=release` in case you want to use the resulting binaries productively. By default, the build happens without optimizations which slows down the generator. ## Hacking Pull-requests and patches are very welcome! Using C++23 features is encouraged, if sensible. Make sure your code compiles in maintainer mode, and format your changes to adhere to the project's coding style. To help with the latter we provide the `autoformat.py` helper script to format code via *clang-format*. appstream-generator-0.10.1/RELEASE000066400000000000000000000020071506754475600165760ustar00rootroot00000000000000AppStream Generator Release Notes 1. Write NEWS entries for AppStream Generator in the same format as usual. git shortlog v0.10.0.. | grep -i -v trivial | grep -v Merge > NEWS.new -------------------------------------------------------------------------------- Version 0.10.1 ~~~~~~~~~~~~~~ Released: 2025-xx-xx Notes: Features: Bugfixes: Miscellaneous: Contributors: -------------------------------------------------------------------------------- 2. Commit changes in Git: git commit -a -m "Release version 0.10.1" git tag -s -f -m "Release 0.10.1" v0.10.1 git push --tags git push 3. Do post release version bump in meson.build, RELEASE 4. Commit trivial changes: git commit -a -m "trivial: post release version bump" git push 5. Send an email to appstream@lists.freedesktop.org ================================================= AppStream Generator 0.10.1 released! Tarballs available here: https://github.com/ximion/appstream-generator/releases ================================================= appstream-generator-0.10.1/TODO000066400000000000000000000004371506754475600162700ustar00rootroot00000000000000= AppStream Generator TODO List = === Known issues === * No persistent problems at time :-) === Planned Features === * Use feature from the upcoming libappstream-compose === Whishlist / Random Ideas === * Add an icon-cache so we don't render SVG icons in themes multiple times. appstream-generator-0.10.1/autoformat.py000077500000000000000000000075041506754475600203400ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (C) 2015-2022 Matthias Klumpp # # SPDX-License-Identifier: LGPL-2.1+ # # Format all AppStream source code in-place. # import os import sys import shutil import fnmatch import subprocess import tempfile from glob import glob INCLUDE_LOCATIONS = [ 'autoformat.py', 'src', 'tests', ] EXCLUDE_MATCH = [] EXTRA_STYLE_RULES_FOR = [] def format_cpp_sources(sources, style_fname=None, extra_styles: list[str] = None): """Format C/C++ sources with clang-format.""" if not sources: return command = ['clang-format', '-i'] if extra_styles: style_rules = [] if style_fname: with open(style_fname, 'r') as f: style_rules = [l.strip() for l in f.readlines()] with tempfile.NamedTemporaryFile(mode='w') as fp: style_rules.extend(extra_styles) fp.write('\n'.join(style_rules)) fp.flush() command.append('--style=file:{}'.format(fp.name)) command.extend(sources) subprocess.run(command, check=True) return if style_fname: command.append('--style=file:{}'.format(style_fname)) command.extend(sources) subprocess.run(command, check=True) def format_python_sources(sources): """Format Python sources with Black.""" command = [ 'black', '-S', # no string normalization '-l', '100', # line length '-t', 'py311', # minimum Python target ] command.extend(sources) subprocess.run(command, check=True) def run(current_dir, args): # check for tools if not shutil.which('clang-format'): print( 'The `clang-format` formatter is not installed. Please install it to continue!', file=sys.stderr, ) return 1 if not shutil.which('black'): print( 'The `black` formatter is not installed. Please install it to continue!', file=sys.stderr, ) return 1 # if no include directories are explicitly specified, we read all locations if not INCLUDE_LOCATIONS: INCLUDE_LOCATIONS.append('.') # collect sources cpp_sources = [] cpp_style_matches = [[]] * len(EXTRA_STYLE_RULES_FOR) py_sources = [] for il_path_base in INCLUDE_LOCATIONS: il_path = os.path.join(current_dir, il_path_base) if os.path.isfile(il_path): candidates = [il_path] else: candidates = glob(il_path + '/**/*', recursive=True) for filename in candidates: skip = False for exclude in EXCLUDE_MATCH: if fnmatch.fnmatch(filename, exclude): skip = True break if skip: continue if filename.endswith(('.c', '.cpp', '.h', '.hpp')): cpp_sources.append(filename) for i, er in enumerate(EXTRA_STYLE_RULES_FOR): for pattern in er[1]: if fnmatch.fnmatch(filename, pattern): cpp_style_matches[i].append(filename) break elif filename.endswith('.py'): py_sources.append(filename) # format format_python_sources(py_sources) format_cpp_sources(cpp_sources) for i, er in enumerate(EXTRA_STYLE_RULES_FOR): format_cpp_sources( cpp_style_matches[i], os.path.join(current_dir, '.clang-format'), er[0], ) return 0 if __name__ == '__main__': thisfile = __file__ if not os.path.isabs(thisfile): thisfile = os.path.normpath(os.path.join(os.getcwd(), thisfile)) thisdir = os.path.normpath(os.path.join(os.path.dirname(thisfile))) os.chdir(thisdir) sys.exit(run(thisdir, sys.argv[1:])) appstream-generator-0.10.1/contrib/000077500000000000000000000000001506754475600172345ustar00rootroot00000000000000appstream-generator-0.10.1/contrib/cleanup-cruft.sh.example000066400000000000000000000013511506754475600237720ustar00rootroot00000000000000#!/bin/bash # # Script cleaning up the AppStream metadata pool and cache. # This script should be run by a cronjob (e.g. every week). # set -e set -o pipefail set -u WORKSPACE_DIR="/srv/appstream/workspace" # only run one instance of the script LOCKFILE="$WORKSPACE_DIR/.lock" cleanup() { rm -f "$LOCKFILE" } if ! lockfile -r8 $LOCKFILE; then echo "aborting AppStream metadata cleanup because $LOCKFILE has already been locked" exit 0 fi trap cleanup 0 # Start logging logdir="$WORKSPACE_DIR/logs/`date "+%Y/%m"`" mkdir -p $logdir NOW=`date "+%d_%H%M"` LOGFILE="$logdir/${NOW}_cleanup.log" exec >> "$LOGFILE" 2>&1 cd $WORKSPACE_DIR # Cleanup superseded data appstream-generator cleanup # finish logging exec > /dev/null 2>&1 appstream-generator-0.10.1/contrib/setup/000077500000000000000000000000001506754475600203745ustar00rootroot00000000000000appstream-generator-0.10.1/contrib/setup/.gitignore000066400000000000000000000000161506754475600223610ustar00rootroot00000000000000node_modules/ appstream-generator-0.10.1/contrib/setup/build_js.sh000077500000000000000000000011701506754475600225250ustar00rootroot00000000000000#!/bin/sh set -e if [ -n "$MESON_SOURCE_ROOT" ]; then cd "$MESON_SOURCE_ROOT/contrib/setup/" fi NPM="npm" if [ ! -z "$1" ] then NPM=$1 fi $NPM ci --ignore-scripts JS_TARGET=../../data/templates/default/static/js [ ! -d "$JS_TARGET" ] && mkdir $JS_TARGET [ ! -d "$JS_TARGET/jquery" ] && mkdir $JS_TARGET/jquery install node_modules/jquery/dist/*.min.js -t $JS_TARGET/jquery [ ! -d "$JS_TARGET/flot" ] && mkdir $JS_TARGET/flot install node_modules/jquery-flot/jquery.flot*.js -t $JS_TARGET/flot [ ! -d "$JS_TARGET/highlight" ] && mkdir $JS_TARGET/highlight install node_modules/highlightjs/*.js -t $JS_TARGET/highlight appstream-generator-0.10.1/contrib/setup/meson-install-templates.sh000077500000000000000000000004101506754475600255070ustar00rootroot00000000000000#!/bin/sh set -e cd "$MESON_SOURCE_ROOT" echo "Installing templates..." install -d "${DESTDIR}/${MESON_INSTALL_PREFIX}/share/appstream/templates" cp -dpru --no-preserve=ownership data/templates/* -t "${DESTDIR}/${MESON_INSTALL_PREFIX}/share/appstream/templates" appstream-generator-0.10.1/contrib/setup/package-lock.json000066400000000000000000000022761506754475600236170ustar00rootroot00000000000000{ "name": "setup", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { "highlightjs": "^9.10.0", "jquery": "^3.3.1", "jquery-flot": "^0.8.3" } }, "node_modules/highlightjs": { "version": "9.16.2", "resolved": "https://registry.npmjs.org/highlightjs/-/highlightjs-9.16.2.tgz", "integrity": "sha512-FK1vmMj8BbEipEy8DLIvp71t5UsC7n2D6En/UfM/91PCwmOpj6f2iu0Y0coRC62KSRHHC+dquM2xMULV/X7NFg==", "deprecated": "Use the 'highlight.js' package instead https://npm.im/highlight.js", "license": "BSD-3-Clause" }, "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", "license": "MIT" }, "node_modules/jquery-flot": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/jquery-flot/-/jquery-flot-0.8.3.tgz", "integrity": "sha512-IkQCsA5t55Aubu8iove/X/KL34rdZTsDyx/bylC7F320N2bwsJhJe3Np8orsx1L8FEOoMDfNe3/pSb3xXKLxeQ==", "deprecated": "flot has been abandoned" } } } appstream-generator-0.10.1/contrib/setup/package.json000066400000000000000000000001761506754475600226660ustar00rootroot00000000000000{ "flat": true, "dependencies": { "highlightjs": "^9.10.0", "jquery": "^3.3.1", "jquery-flot": "^0.8.3" } } appstream-generator-0.10.1/contrib/subprojects/000077500000000000000000000000001506754475600215775ustar00rootroot00000000000000appstream-generator-0.10.1/contrib/subprojects/.gitignore000066400000000000000000000000561506754475600235700ustar00rootroot00000000000000inja/ packagecache/ backward-cpp-*/ .wraplock appstream-generator-0.10.1/contrib/subprojects/backward-cpp.wrap000066400000000000000000000012411506754475600250260ustar00rootroot00000000000000[wrap-file] directory = backward-cpp-1.6 source_url = https://github.com/bombela/backward-cpp/archive/refs/tags/v1.6.tar.gz source_filename = backward-cpp-1.6.tar.gz source_hash = c654d0923d43f1cea23d086729673498e4741fb2457e806cfaeaea7b20c97c10 patch_filename = backward-cpp_1.6-6_patch.zip patch_url = https://wrapdb.mesonbuild.com/v2/backward-cpp_1.6-6/get_patch patch_hash = 2c952611584971cdcb04b5c4aeca3401f1fba9544e6203e08b933fd9c21427fc source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/backward-cpp_1.6-6/backward-cpp-1.6.tar.gz wrapdb_version = 1.6-6 [provide] backward-cpp = backward_dep backward-cpp-interface = backward_interface_dep appstream-generator-0.10.1/contrib/subprojects/inja.wrap000066400000000000000000000001271506754475600234130ustar00rootroot00000000000000[wrap-git] directory = inja url = https://github.com/pantor/inja.git revision = v3.4.0 appstream-generator-0.10.1/contrib/subprojects/nlohmann_json.wrap000066400000000000000000000007351506754475600253420ustar00rootroot00000000000000[wrap-file] directory = nlohmann_json-3.12.0 lead_directory_missing = true source_url = https://github.com/nlohmann/json/releases/download/v3.12.0/include.zip source_filename = nlohmann_json-3.12.0.zip source_hash = b8cb0ef2dd7f57f18933997c9934bb1fa962594f701cd5a8d3c2c80541559372 source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/nlohmann_json_3.12.0-1/nlohmann_json-3.12.0.zip wrapdb_version = 3.12.0-1 [provide] nlohmann_json = nlohmann_json_dep appstream-generator-0.10.1/contrib/update-metadata.sh.example000066400000000000000000000017151506754475600242660ustar00rootroot00000000000000#!/bin/bash # # Example script for updating AppStream metadata using appstream-generator. # This script can easily be run by a cronjob. # set -e set -o pipefail set -u SUITES="sid stretch" WORKSPACE_DIR="/srv/appstream/workspace" PUBLIC_DIR="/srv/appstream/public" # only run one instance of the script LOCKFILE="$WORKSPACE_DIR/.lock" cleanup() { rm -f "$LOCKFILE" } if ! lockfile -r8 $LOCKFILE; then echo "aborting AppStream metadata extraction because $LOCKFILE has already been locked" exit 0 fi trap cleanup 0 # Start logging logdir="$WORKSPACE_DIR/logs/`date "+%Y/%m"`" mkdir -p $logdir NOW=`date "+%d_%H%M"` LOGFILE="$logdir/${NOW}.log" exec >> "$LOGFILE" 2>&1 cd $WORKSPACE_DIR # generate fresh metadata for suite in $SUITES; do appstream-generator process $suite done # Sync updated data to public directory rsync -ak --delete-after --link-dest="$PUBLIC_DIR/" "$WORKSPACE_DIR/export/" "$PUBLIC_DIR/" # finish logging exec > /dev/null 2>&1 appstream-generator-0.10.1/data/000077500000000000000000000000001506754475600165055ustar00rootroot00000000000000appstream-generator-0.10.1/data/.gitignore000066400000000000000000000001551506754475600204760ustar00rootroot00000000000000templates/default/static/js/jquery/ templates/default/static/js/flot/ templates/default/static/js/highlight/ appstream-generator-0.10.1/data/asgen-hints.json000066400000000000000000000153551506754475600216310ustar00rootroot00000000000000{ "internal-error": { "text": [ "A fatal problem appeared in the generator.", "Please report a bug: {{msg}}"], "severity": "error" }, "internal-unknown-tag": { "text": ["The generator emitted a tag '{{tag}}' which is unknown. This is a bug in the metadata generator, please", "file a bugreport."], "severity": "warning" }, "icon-format-unsupported": { "text": "Icon file '{{icon_fname}}' uses an unsupported image file format.", "severity": "error" }, "icon-not-found": { "text": [ "The icon '{{icon_fname}}' was not found in the archive. This issue can have multiple reasons:", "
    ", "
  • The icon is not present in the archive.
  • ", "
  • The icon is in a wrong directory.
  • ", "
  • The icon is not available in a suitable size (at least 64x64px)
  • ", "
  • On Debian and Ubuntu, the icon is a symlink. The generator cannot read symlinks on these distributions - make the icon a real file.
  • ", "
", "To make the icon easier to find, place it in /usr/share/icons/hicolor/<size>/apps and ensure the Icon= value", "of the .desktop file is set correctly." ], "severity": "error" }, "icon-scaled-up": { "text": "Icon file '{{icon_name}}' was scaled up from {{icon_size}}px to {{scale_size}}px. Please try to supply a bigger icon.", "severity": "warning" }, "icon-too-small": { "text": [ "Only a very small icon ('{{icon_name}}', {{icon_size}}px) could be located.", "Please try to supply a bigger icon (at least 64x64px) in the Freedesktop icon path (/usr/share/icons/hicolor/%size%/apps/)." ], "severity": "error" }, "pkg-extract-error": { "text": "Could not extract file '{{fname}}' from package '{{pkg_fname}}'. Error: {{error}}", "severity": "error" }, "pkg-empty-file": { "text": ["Could not extract file '{{fname}}' from package '{{pkg_fname}}'.", "Icon data was empty. The icon might be a symbolic link pointing at a file outside of this package.", "Please do not do that and instead place the icons in their appropriate directories in /usr/share/icons/hicolor/."], "severity": "error" }, "image-write-error": { "text": ["Could write new image generated from '{{fname}}' (package '{{pkg_fname}}'): {{error}}"], "severity": "error" }, "metainfo-validation-error": { "text": "Validation of the MetaInfo file failed: {{msg}}", "severity": "warning" }, "no-install-candidate": { "text": "Component has no install candidate defined. A package must be associated with a package or bundle providing it.", "severity": "error" }, "metainfo-duplicate-id": { "text": ["The component-id '{{cid}}' already appeared in package '{{pkgname}}'. AppStream-IDs must be unique, please resolve which package will be", "providing this component by default.
", "This issue may happen temporarily when metadata is moved from one package to another. In that case, ignore this issue, it will vanish soon."], "severity": "error" }, "missing-desktop-file": { "text": ["Found an AppStream MetaInfo XML file, but the associated .desktop file is missing. This often happens when the .desktop file is renamed, but the", "<launchable type=\"desktop-id\"/> tag value of the AppStream MetaInfo file is not adapted as well, or if the MetaInfo file is located in a different package than the .desktop file.
", "Please fix the packaging or work with upstream to resolve this issue.
", "For older metadata, the desktop-id is inferred from the <id/> tag. If the component metadata has no launchable tag and no", "icon tag of type stock, check if a .desktop file named after the component-ID is located in the same package."], "severity": "error" }, "description-from-package": { "text": ["This software component gets its description from the package it is located in.
", "This has several disadvantages, like poor markup, too technical descriptions for users of software centers, different components having the same description, etc.
", "Please consider to either hide this .desktop file from AppStream by adding a X-AppStream-Ignore=true field to its .desktop file, or to write a MetaInfo file for ", "this component to take the long description upstream. In future, components without MetaInfo file might be dropped from the metadata entirely.", "You can consult the XML quickstart guides for more information on how to write a MetaInfo file."], "severity": "info" }, "no-metainfo": { "text": ["This software component is missing a MetaInfo file to provide metadata.
", "We currently took some data from its desktop-entry file and the long description of the package it is located in.
", "This has several disadvantages, like poor markup, too technical descriptions for users of software centers, different components having the same description, etc.
", "Additionally, a lot of software with desktop-entry files should either not be installable and searchable via the software catalog (like desktop-specific settings applications) or be tagged accordingly via MetaInfo files.
", "Please consider to either hide this desktop-entry file from AppStream by adding a X-AppStream-Ignore=true field to it, or to write a MetaInfo file for this component and send it upstream.
", "Generating components from non-MetaInfo files is deprecated, if you do not add a MetaInfo file, ", "this software may vanish from the metadata catalog (and if it is a GUI application, no longer be visible in software centers) in a future distribution release.
", "You can consult the MetaInfo quickstart guides for more information on how to write a MetaInfo file, ", "or file a bug with the upstream author of this software component."], "severity": "warning" }, "metadata-serialization-failed": { "text": ["Could not create the final metadata. This could have many causes, including a generator bug, but the likeliest cause is an error in the input metadata.
", "The error message was: {{msg}}"], "severity": "error" } } appstream-generator-0.10.1/data/asgen-logo.svg000066400000000000000000000556251506754475600212760ustar00rootroot00000000000000 image/svg+xml appstream-generator-0.10.1/data/hicolor-theme-index.theme000066400000000000000000001543231506754475600234050ustar00rootroot00000000000000[Icon Theme] Name=Hicolor Comment=Fallback icon theme Hidden=true Directories=16x16/actions,16x16@2/actions,16x16/animations,16x16@2/animations,16x16/apps,16x16@2/apps,16x16/categories,16x16@2/categories,16x16/devices,16x16@2/devices,16x16/emblems,16x16@2/emblems,16x16/emotes,16x16@2/emotes,16x16/filesystems,16x16@2/filesystems,16x16/intl,16x16@2/intl,16x16/mimetypes,16x16@2/mimetypes,16x16/places,16x16@2/places,16x16/status,16x16@2/status,16x16/stock/chart,16x16@2/stock/chart,16x16/stock/code,16x16@2/stock/code,16x16/stock/data,16x16@2/stock/data,16x16/stock/form,16x16@2/stock/form,16x16/stock/image,16x16@2/stock/image,16x16/stock/io,16x16@2/stock/io,16x16/stock/media,16x16@2/stock/media,16x16/stock/navigation,16x16@2/stock/navigation,16x16/stock/net,16x16@2/stock/net,16x16/stock/object,16x16@2/stock/object,16x16/stock/table,16x16@2/stock/table,16x16/stock/text,16x16@2/stock/text,22x22/actions,22x22@2/actions,22x22/animations,22x22@2/animations,22x22/apps,22x22@2/apps,22x22/categories,22x22@2/categories,22x22/devices,22x22@2/devices,22x22/emblems,22x22@2/emblems,22x22/emotes,22x22@2/emotes,22x22/filesystems,22x22@2/filesystems,22x22/intl,22x22@2/intl,22x22/mimetypes,22x22@2/mimetypes,22x22/places,22x22@2/places,22x22/status,22x22@2/status,22x22/stock/chart,22x22@2/stock/chart,22x22/stock/code,22x22@2/stock/code,22x22/stock/data,22x22@2/stock/data,22x22/stock/form,22x22@2/stock/form,22x22/stock/image,22x22@2/stock/image,22x22/stock/io,22x22@2/stock/io,22x22/stock/media,22x22@2/stock/media,22x22/stock/navigation,22x22@2/stock/navigation,22x22/stock/net,22x22@2/stock/net,22x22/stock/object,22x22@2/stock/object,22x22/stock/table,22x22@2/stock/table,22x22/stock/text,22x22@2/stock/text,24x24/actions,24x24@2/actions,24x24/animations,24x24@2/animations,24x24/apps,24x24@2/apps,24x24/categories,24x24@2/categories,24x24/devices,24x24@2/devices,24x24/emblems,24x24@2/emblems,24x24/emotes,24x24@2/emotes,24x24/filesystems,24x24@2/filesystems,24x24/intl,24x24@2/intl,24x24/mimetypes,24x24@2/mimetypes,24x24/places,24x24@2/places,24x24/status,24x24@2/status,24x24/stock/chart,24x24@2/stock/chart,24x24/stock/code,24x24@2/stock/code,24x24/stock/data,24x24@2/stock/data,24x24/stock/form,24x24@2/stock/form,24x24/stock/image,24x24@2/stock/image,24x24/stock/io,24x24@2/stock/io,24x24/stock/media,24x24@2/stock/media,24x24/stock/navigation,24x24@2/stock/navigation,24x24/stock/net,24x24@2/stock/net,24x24/stock/object,24x24@2/stock/object,24x24/stock/table,24x24@2/stock/table,24x24/stock/text,24x24@2/stock/text,32x32/actions,32x32@2/actions,32x32/animations,32x32@2/animations,32x32/apps,32x32@2/apps,32x32/categories,32x32@2/categories,32x32/devices,32x32@2/devices,32x32/emblems,32x32@2/emblems,32x32/emotes,32x32@2/emotes,32x32/filesystems,32x32@2/filesystems,32x32/intl,32x32@2/intl,32x32/mimetypes,32x32@2/mimetypes,32x32/places,32x32@2/places,32x32/status,32x32@2/status,32x32/stock/chart,32x32@2/stock/chart,32x32/stock/code,32x32@2/stock/code,32x32/stock/data,32x32@2/stock/data,32x32/stock/form,32x32@2/stock/form,32x32/stock/image,32x32@2/stock/image,32x32/stock/io,32x32@2/stock/io,32x32/stock/media,32x32@2/stock/media,32x32/stock/navigation,32x32@2/stock/navigation,32x32/stock/net,32x32@2/stock/net,32x32/stock/object,32x32@2/stock/object,32x32/stock/table,32x32@2/stock/table,32x32/stock/text,32x32@2/stock/text,36x36/actions,36x36@2/actions,36x36/animations,36x36@2/animations,36x36/apps,36x36@2/apps,36x36/categories,36x36@2/categories,36x36/devices,36x36@2/devices,36x36/emblems,36x36@2/emblems,36x36/emotes,36x36@2/emotes,36x36/filesystems,36x36@2/filesystems,36x36/intl,36x36@2/intl,36x36/mimetypes,36x36@2/mimetypes,36x36/places,36x36@2/places,36x36/status,36x36@2/status,36x36/stock/chart,36x36@2/stock/chart,36x36/stock/code,36x36@2/stock/code,36x36/stock/data,36x36@2/stock/data,36x36/stock/form,36x36@2/stock/form,36x36/stock/image,36x36@2/stock/image,36x36/stock/io,36x36@2/stock/io,36x36/stock/media,36x36@2/stock/media,36x36/stock/navigation,36x36@2/stock/navigation,36x36/stock/net,36x36@2/stock/net,36x36/stock/object,36x36@2/stock/object,36x36/stock/table,36x36@2/stock/table,36x36/stock/text,36x36@2/stock/text,48x48/actions,48x48@2/actions,48x48/animations,48x48@2/animations,48x48/apps,48x48@2/apps,48x48/categories,48x48@2/categories,48x48/devices,48x48@2/devices,48x48/emblems,48x48@2/emblems,48x48/emotes,48x48@2/emotes,48x48/filesystems,48x48@2/filesystems,48x48/intl,48x48@2/intl,48x48/mimetypes,48x48@2/mimetypes,48x48/places,48x48@2/places,48x48/status,48x48@2/status,48x48/stock/chart,48x48@2/stock/chart,48x48/stock/code,48x48@2/stock/code,48x48/stock/data,48x48@2/stock/data,48x48/stock/form,48x48@2/stock/form,48x48/stock/image,48x48@2/stock/image,48x48/stock/io,48x48@2/stock/io,48x48/stock/media,48x48@2/stock/media,48x48/stock/navigation,48x48@2/stock/navigation,48x48/stock/net,48x48@2/stock/net,48x48/stock/object,48x48@2/stock/object,48x48/stock/table,48x48@2/stock/table,48x48/stock/text,48x48@2/stock/text,64x64/actions,64x64@2/actions,64x64/animations,64x64@2/animations,64x64/apps,64x64@2/apps,64x64/categories,64x64@2/categories,64x64/devices,64x64@2/devices,64x64/emblems,64x64@2/emblems,64x64/emotes,64x64@2/emotes,64x64/filesystems,64x64@2/filesystems,64x64/intl,64x64@2/intl,64x64/mimetypes,64x64@2/mimetypes,64x64/places,64x64@2/places,64x64/status,64x64@2/status,64x64/stock/chart,64x64@2/stock/chart,64x64/stock/code,64x64@2/stock/code,64x64/stock/data,64x64@2/stock/data,64x64/stock/form,64x64@2/stock/form,64x64/stock/image,64x64@2/stock/image,64x64/stock/io,64x64@2/stock/io,64x64/stock/media,64x64@2/stock/media,64x64/stock/navigation,64x64@2/stock/navigation,64x64/stock/net,64x64@2/stock/net,64x64/stock/object,64x64@2/stock/object,64x64/stock/table,64x64@2/stock/table,64x64/stock/text,64x64@2/stock/text,72x72/actions,72x72@2/actions,72x72/animations,72x72@2/animations,72x72/apps,72x72@2/apps,72x72/categories,72x72@2/categories,72x72/devices,72x72@2/devices,72x72/emblems,72x72@2/emblems,72x72/emotes,72x72@2/emotes,72x72/filesystems,72x72@2/filesystems,72x72/intl,72x72@2/intl,72x72/mimetypes,72x72@2/mimetypes,72x72/places,72x72@2/places,72x72/status,72x72@2/status,72x72/stock/chart,72x72@2/stock/chart,72x72/stock/code,72x72@2/stock/code,72x72/stock/data,72x72@2/stock/data,72x72/stock/form,72x72@2/stock/form,72x72/stock/image,72x72@2/stock/image,72x72/stock/io,72x72@2/stock/io,72x72/stock/media,72x72@2/stock/media,72x72/stock/navigation,72x72@2/stock/navigation,72x72/stock/net,72x72@2/stock/net,72x72/stock/object,72x72@2/stock/object,72x72/stock/table,72x72@2/stock/table,72x72/stock/text,72x72@2/stock/text,96x96/actions,96x96@2/actions,96x96/animations,96x96@2/animations,96x96/apps,96x96@2/apps,96x96/categories,96x96@2/categories,96x96/devices,96x96@2/devices,96x96/emblems,96x96@2/emblems,96x96/emotes,96x96@2/emotes,96x96/filesystems,96x96@2/filesystems,96x96/intl,96x96@2/intl,96x96/mimetypes,96x96@2/mimetypes,96x96/places,96x96@2/places,96x96/status,96x96@2/status,96x96/stock/chart,96x96@2/stock/chart,96x96/stock/code,96x96@2/stock/code,96x96/stock/data,96x96@2/stock/data,96x96/stock/form,96x96@2/stock/form,96x96/stock/image,96x96@2/stock/image,96x96/stock/io,96x96@2/stock/io,96x96/stock/media,96x96@2/stock/media,96x96/stock/navigation,96x96@2/stock/navigation,96x96/stock/net,96x96@2/stock/net,96x96/stock/object,96x96@2/stock/object,96x96/stock/table,96x96@2/stock/table,96x96/stock/text,96x96@2/stock/text,128x128/actions,128x128@2/actions,128x128/animations,128x128@2/animations,128x128/apps,128x128@2/apps,128x128/categories,128x128@2/categories,128x128/devices,128x128@2/devices,128x128/emblems,128x128@2/emblems,128x128/emotes,128x128@2/emotes,128x128/filesystems,128x128@2/filesystems,128x128/intl,128x128@2/intl,128x128/mimetypes,128x128@2/mimetypes,128x128/places,128x128@2/places,128x128/status,128x128@2/status,128x128/stock/chart,128x128@2/stock/chart,128x128/stock/code,128x128@2/stock/code,128x128/stock/data,128x128@2/stock/data,128x128/stock/form,128x128@2/stock/form,128x128/stock/image,128x128@2/stock/image,128x128/stock/io,128x128@2/stock/io,128x128/stock/media,128x128@2/stock/media,128x128/stock/navigation,128x128@2/stock/navigation,128x128/stock/net,128x128@2/stock/net,128x128/stock/object,128x128@2/stock/object,128x128/stock/table,128x128@2/stock/table,128x128/stock/text,128x128@2/stock/text,192x192/actions,192x192@2/actions,192x192/animations,192x192@2/animations,192x192/apps,192x192@2/apps,192x192/categories,192x192@2/categories,192x192/devices,192x192@2/devices,192x192/emblems,192x192@2/emblems,192x192/emotes,192x192@2/emotes,192x192/filesystems,192x192@2/filesystems,192x192/intl,192x192@2/intl,192x192/mimetypes,192x192@2/mimetypes,192x192/places,192x192@2/places,192x192/status,192x192@2/status,192x192/stock/chart,192x192@2/stock/chart,192x192/stock/code,192x192@2/stock/code,192x192/stock/data,192x192@2/stock/data,192x192/stock/form,192x192@2/stock/form,192x192/stock/image,192x192@2/stock/image,192x192/stock/io,192x192@2/stock/io,192x192/stock/media,192x192@2/stock/media,192x192/stock/navigation,192x192@2/stock/navigation,192x192/stock/net,192x192@2/stock/net,192x192/stock/object,192x192@2/stock/object,192x192/stock/table,192x192@2/stock/table,192x192/stock/text,192x192@2/stock/text,256x256/actions,256x256@2/actions,256x256/animations,256x256@2/animations,256x256/apps,256x256@2/apps,256x256/categories,256x256@2/categories,256x256/devices,256x256@2/devices,256x256/emblems,256x256@2/emblems,256x256/emotes,256x256@2/emotes,256x256/filesystems,256x256@2/filesystems,256x256/intl,256x256@2/intl,256x256/mimetypes,256x256@2/mimetypes,256x256/places,256x256@2/places,256x256/status,256x256@2/status,256x256/stock/chart,256x256@2/stock/chart,256x256/stock/code,256x256@2/stock/code,256x256/stock/data,256x256@2/stock/data,256x256/stock/form,256x256@2/stock/form,256x256/stock/image,256x256@2/stock/image,256x256/stock/io,256x256@2/stock/io,256x256/stock/media,256x256@2/stock/media,256x256/stock/navigation,256x256@2/stock/navigation,256x256/stock/net,256x256@2/stock/net,256x256/stock/object,256x256@2/stock/object,256x256/stock/table,256x256@2/stock/table,256x256/stock/text,256x256@2/stock/text,512x512/actions,512x512@2/actions,512x512/animations,512x512@2/animations,512x512/apps,512x512@2/apps,512x512/categories,512x512@2/categories,512x512/devices,512x512@2/devices,512x512/emblems,512x512@2/emblems,512x512/emotes,512x512@2/emotes,512x512/filesystems,512x512@2/filesystems,512x512/intl,512x512@2/intl,512x512/mimetypes,512x512@2/mimetypes,512x512/places,512x512@2/places,512x512/status,512x512@2/status,512x512/stock/chart,512x512@2/stock/chart,512x512/stock/code,512x512@2/stock/code,512x512/stock/data,512x512@2/stock/data,512x512/stock/form,512x512@2/stock/form,512x512/stock/image,512x512@2/stock/image,512x512/stock/io,512x512@2/stock/io,512x512/stock/media,512x512@2/stock/media,512x512/stock/navigation,512x512@2/stock/navigation,512x512/stock/net,512x512@2/stock/net,512x512/stock/object,512x512@2/stock/object,512x512/stock/table,512x512@2/stock/table,512x512/stock/text,512x512@2/stock/text,scalable/actions,scalable/animations,scalable/apps,scalable/categories,scalable/devices,scalable/emblems,scalable/emotes,scalable/filesystems,scalable/intl,scalable/mimetypes,scalable/places,scalable/status,scalable/stock/chart,scalable/stock/code,scalable/stock/data,scalable/stock/form,scalable/stock/image,scalable/stock/io,scalable/stock/media,scalable/stock/navigation,scalable/stock/net,scalable/stock/object,scalable/stock/table,scalable/stock/text,symbolic/apps [16x16/actions] Size=16 Context=Actions Type=Threshold [16x16@2/actions] Size=16 Scale=2 Context=Actions Type=Threshold [16x16/animations] Size=16 Context=Animations Type=Threshold [16x16@2/animations] Size=16 Scale=2 Context=Animations Type=Threshold [16x16/apps] Size=16 Context=Applications Type=Threshold [16x16@2/apps] Size=16 Scale=2 Context=Applications Type=Threshold [16x16/categories] Size=16 Context=Categories Type=Threshold [16x16@2/categories] Size=16 Scale=2 Context=Categories Type=Threshold [16x16/devices] Size=16 Context=Devices Type=Threshold [16x16@2/devices] Size=16 Scale=2 Context=Devices Type=Threshold [16x16/emblems] Size=16 Context=Emblems Type=Threshold [16x16@2/emblems] Size=16 Scale=2 Context=Emblems Type=Threshold [16x16/emotes] Size=16 Context=Emotes Type=Threshold [16x16@2/emotes] Size=16 Scale=2 Context=Emotes Type=Threshold [16x16/filesystems] Size=16 Context=FileSystems Type=Threshold [16x16@2/filesystems] Size=16 Scale=2 Context=FileSystems Type=Threshold [16x16/intl] Size=16 Context=International Type=Threshold [16x16@2/intl] Size=16 Scale=2 Context=International Type=Threshold [16x16/mimetypes] Size=16 Context=MimeTypes Type=Threshold [16x16@2/mimetypes] Size=16 Scale=2 Context=MimeTypes Type=Threshold [16x16/places] Size=16 Context=Places Type=Threshold [16x16@2/places] Size=16 Scale=2 Context=Places Type=Threshold [16x16/status] Size=16 Context=Status Type=Threshold [16x16@2/status] Size=16 Scale=2 Context=Status Type=Threshold [16x16/stock/chart] Size=16 Context=Stock Type=Threshold [16x16@2/stock/chart] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/code] Size=16 Context=Stock Type=Threshold [16x16@2/stock/code] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/data] Size=16 Context=Stock Type=Threshold [16x16@2/stock/data] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/form] Size=16 Context=Stock Type=Threshold [16x16@2/stock/form] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/image] Size=16 Context=Stock Type=Threshold [16x16@2/stock/image] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/io] Size=16 Context=Stock Type=Threshold [16x16@2/stock/io] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/media] Size=16 Context=Stock Type=Threshold [16x16@2/stock/media] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/navigation] Size=16 Context=Stock Type=Threshold [16x16@2/stock/navigation] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/net] Size=16 Context=Stock Type=Threshold [16x16@2/stock/net] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/object] Size=16 Context=Stock Type=Threshold [16x16@2/stock/object] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/table] Size=16 Context=Stock Type=Threshold [16x16@2/stock/table] Size=16 Scale=2 Context=Stock Type=Threshold [16x16/stock/text] Size=16 Context=Stock Type=Threshold [16x16@2/stock/text] Size=16 Scale=2 Context=Stock Type=Threshold [22x22/actions] Size=22 Context=Actions Type=Threshold [22x22@2/actions] Size=22 Scale=2 Context=Actions Type=Threshold [22x22/animations] Size=22 Context=Animations Type=Threshold [22x22@2/animations] Size=22 Scale=2 Context=Animations Type=Threshold [22x22/apps] Size=22 Context=Applications Type=Threshold [22x22@2/apps] Size=22 Scale=2 Context=Applications Type=Threshold [22x22/categories] Size=22 Context=Categories Type=Threshold [22x22@2/categories] Size=22 Scale=2 Context=Categories Type=Threshold [22x22/devices] Size=22 Context=Devices Type=Threshold [22x22@2/devices] Size=22 Scale=2 Context=Devices Type=Threshold [22x22/emblems] Size=22 Context=Emblems Type=Threshold [22x22@2/emblems] Size=22 Scale=2 Context=Emblems Type=Threshold [22x22/emotes] Size=22 Context=Emotes Type=Threshold [22x22@2/emotes] Size=22 Scale=2 Context=Emotes Type=Threshold [22x22/filesystems] Size=22 Context=FileSystems Type=Threshold [22x22@2/filesystems] Size=22 Scale=2 Context=FileSystems Type=Threshold [22x22/intl] Size=22 Context=International Type=Threshold [22x22@2/intl] Size=22 Scale=2 Context=International Type=Threshold [22x22/mimetypes] Size=22 Context=MimeTypes Type=Threshold [22x22@2/mimetypes] Size=22 Scale=2 Context=MimeTypes Type=Threshold [22x22/places] Size=22 Context=Places Type=Threshold [22x22@2/places] Size=22 Scale=2 Context=Places Type=Threshold [22x22/status] Size=22 Context=Status Type=Threshold [22x22@2/status] Size=22 Scale=2 Context=Status Type=Threshold [22x22/stock/chart] Size=22 Context=Stock Type=Threshold [22x22@2/stock/chart] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/code] Size=22 Context=Stock Type=Threshold [22x22@2/stock/code] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/data] Size=22 Context=Stock Type=Threshold [22x22@2/stock/data] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/form] Size=22 Context=Stock Type=Threshold [22x22@2/stock/form] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/image] Size=22 Context=Stock Type=Threshold [22x22@2/stock/image] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/io] Size=22 Context=Stock Type=Threshold [22x22@2/stock/io] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/media] Size=22 Context=Stock Type=Threshold [22x22@2/stock/media] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/navigation] Size=22 Context=Stock Type=Threshold [22x22@2/stock/navigation] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/net] Size=22 Context=Stock Type=Threshold [22x22@2/stock/net] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/object] Size=22 Context=Stock Type=Threshold [22x22@2/stock/object] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/table] Size=22 Context=Stock Type=Threshold [22x22@2/stock/table] Size=22 Scale=2 Context=Stock Type=Threshold [22x22/stock/text] Size=22 Context=Stock Type=Threshold [22x22@2/stock/text] Size=22 Scale=2 Context=Stock Type=Threshold [24x24/actions] Size=24 Context=Actions Type=Threshold [24x24@2/actions] Size=24 Scale=2 Context=Actions Type=Threshold [24x24/animations] Size=24 Context=Animations Type=Threshold [24x24@2/animations] Size=24 Scale=2 Context=Animations Type=Threshold [24x24/apps] Size=24 Context=Applications Type=Threshold [24x24@2/apps] Size=24 Scale=2 Context=Applications Type=Threshold [24x24/categories] Size=24 Context=Categories Type=Threshold [24x24@2/categories] Size=24 Scale=2 Context=Categories Type=Threshold [24x24/devices] Size=24 Context=Devices Type=Threshold [24x24@2/devices] Size=24 Scale=2 Context=Devices Type=Threshold [24x24/emblems] Size=24 Context=Emblems Type=Threshold [24x24@2/emblems] Size=24 Scale=2 Context=Emblems Type=Threshold [24x24/emotes] Size=24 Context=Emotes Type=Threshold [24x24@2/emotes] Size=24 Scale=2 Context=Emotes Type=Threshold [24x24/filesystems] Size=24 Context=FileSystems Type=Threshold [24x24@2/filesystems] Size=24 Scale=2 Context=FileSystems Type=Threshold [24x24/intl] Size=24 Context=International Type=Threshold [24x24@2/intl] Size=24 Scale=2 Context=International Type=Threshold [24x24/mimetypes] Size=24 Context=MimeTypes Type=Threshold [24x24@2/mimetypes] Size=24 Scale=2 Context=MimeTypes Type=Threshold [24x24/places] Size=24 Context=Places Type=Threshold [24x24@2/places] Size=24 Scale=2 Context=Places Type=Threshold [24x24/status] Size=24 Context=Status Type=Threshold [24x24@2/status] Size=24 Scale=2 Context=Status Type=Threshold [24x24/stock/chart] Size=24 Context=Stock Type=Threshold [24x24@2/stock/chart] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/code] Size=24 Context=Stock Type=Threshold [24x24@2/stock/code] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/data] Size=24 Context=Stock Type=Threshold [24x24@2/stock/data] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/form] Size=24 Context=Stock Type=Threshold [24x24@2/stock/form] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/image] Size=24 Context=Stock Type=Threshold [24x24@2/stock/image] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/io] Size=24 Context=Stock Type=Threshold [24x24@2/stock/io] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/media] Size=24 Context=Stock Type=Threshold [24x24@2/stock/media] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/navigation] Size=24 Context=Stock Type=Threshold [24x24@2/stock/navigation] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/net] Size=24 Context=Stock Type=Threshold [24x24@2/stock/net] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/object] Size=24 Context=Stock Type=Threshold [24x24@2/stock/object] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/table] Size=24 Context=Stock Type=Threshold [24x24@2/stock/table] Size=24 Scale=2 Context=Stock Type=Threshold [24x24/stock/text] Size=24 Context=Stock Type=Threshold [24x24@2/stock/text] Size=24 Scale=2 Context=Stock Type=Threshold [32x32/actions] Size=32 Context=Actions Type=Threshold [32x32@2/actions] Size=32 Scale=2 Context=Actions Type=Threshold [32x32/animations] Size=32 Context=Animations Type=Threshold [32x32@2/animations] Size=32 Scale=2 Context=Animations Type=Threshold [32x32/apps] Size=32 Context=Applications Type=Threshold [32x32@2/apps] Size=32 Scale=2 Context=Applications Type=Threshold [32x32/categories] Size=32 Context=Categories Type=Threshold [32x32@2/categories] Size=32 Scale=2 Context=Categories Type=Threshold [32x32/devices] Size=32 Context=Devices Type=Threshold [32x32@2/devices] Size=32 Scale=2 Context=Devices Type=Threshold [32x32/emblems] Size=32 Context=Emblems Type=Threshold [32x32@2/emblems] Size=32 Scale=2 Context=Emblems Type=Threshold [32x32/emotes] Size=32 Context=Emotes Type=Threshold [32x32@2/emotes] Size=32 Scale=2 Context=Emotes Type=Threshold [32x32/filesystems] Size=32 Context=FileSystems Type=Threshold [32x32@2/filesystems] Size=32 Scale=2 Context=FileSystems Type=Threshold [32x32/intl] Size=32 Context=International Type=Threshold [32x32@2/intl] Size=32 Scale=2 Context=International Type=Threshold [32x32/mimetypes] Size=32 Context=MimeTypes Type=Threshold [32x32@2/mimetypes] Size=32 Scale=2 Context=MimeTypes Type=Threshold [32x32/places] Size=32 Context=Places Type=Threshold [32x32@2/places] Size=32 Scale=2 Context=Places Type=Threshold [32x32/status] Size=32 Context=Status Type=Threshold [32x32@2/status] Size=32 Scale=2 Context=Status Type=Threshold [32x32/stock/chart] Size=32 Context=Stock Type=Threshold [32x32@2/stock/chart] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/code] Size=32 Context=Stock Type=Threshold [32x32@2/stock/code] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/data] Size=32 Context=Stock Type=Threshold [32x32@2/stock/data] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/form] Size=32 Context=Stock Type=Threshold [32x32@2/stock/form] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/image] Size=32 Context=Stock Type=Threshold [32x32@2/stock/image] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/io] Size=32 Context=Stock Type=Threshold [32x32@2/stock/io] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/media] Size=32 Context=Stock Type=Threshold [32x32@2/stock/media] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/navigation] Size=32 Context=Stock Type=Threshold [32x32@2/stock/navigation] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/net] Size=32 Context=Stock Type=Threshold [32x32@2/stock/net] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/object] Size=32 Context=Stock Type=Threshold [32x32@2/stock/object] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/table] Size=32 Context=Stock Type=Threshold [32x32@2/stock/table] Size=32 Scale=2 Context=Stock Type=Threshold [32x32/stock/text] Size=32 Context=Stock Type=Threshold [32x32@2/stock/text] Size=32 Scale=2 Context=Stock Type=Threshold [36x36/actions] Size=36 Context=Actions Type=Threshold [36x36@2/actions] Size=36 Scale=2 Context=Actions Type=Threshold [36x36/animations] Size=36 Context=Animations Type=Threshold [36x36@2/animations] Size=36 Scale=2 Context=Animations Type=Threshold [36x36/apps] Size=36 Context=Applications Type=Threshold [36x36@2/apps] Size=36 Scale=2 Context=Applications Type=Threshold [36x36/categories] Size=36 Context=Categories Type=Threshold [36x36@2/categories] Size=36 Scale=2 Context=Categories Type=Threshold [36x36/devices] Size=36 Context=Devices Type=Threshold [36x36@2/devices] Size=36 Scale=2 Context=Devices Type=Threshold [36x36/emblems] Size=36 Context=Emblems Type=Threshold [36x36@2/emblems] Size=36 Scale=2 Context=Emblems Type=Threshold [36x36/emotes] Size=36 Context=Emotes Type=Threshold [36x36@2/emotes] Size=36 Scale=2 Context=Emotes Type=Threshold [36x36/filesystems] Size=36 Context=FileSystems Type=Threshold [36x36@2/filesystems] Size=36 Scale=2 Context=FileSystems Type=Threshold [36x36/intl] Size=36 Context=International Type=Threshold [36x36@2/intl] Size=36 Scale=2 Context=International Type=Threshold [36x36/mimetypes] Size=36 Context=MimeTypes Type=Threshold [36x36@2/mimetypes] Size=36 Scale=2 Context=MimeTypes Type=Threshold [36x36/places] Size=36 Context=Places Type=Threshold [36x36@2/places] Size=36 Scale=2 Context=Places Type=Threshold [36x36/status] Size=36 Context=Status Type=Threshold [36x36@2/status] Size=36 Scale=2 Context=Status Type=Threshold [36x36/stock/chart] Size=36 Context=Stock Type=Threshold [36x36@2/stock/chart] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/code] Size=36 Context=Stock Type=Threshold [36x36@2/stock/code] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/data] Size=36 Context=Stock Type=Threshold [36x36@2/stock/data] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/form] Size=36 Context=Stock Type=Threshold [36x36@2/stock/form] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/image] Size=36 Context=Stock Type=Threshold [36x36@2/stock/image] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/io] Size=36 Context=Stock Type=Threshold [36x36@2/stock/io] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/media] Size=36 Context=Stock Type=Threshold [36x36@2/stock/media] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/navigation] Size=36 Context=Stock Type=Threshold [36x36@2/stock/navigation] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/net] Size=36 Context=Stock Type=Threshold [36x36@2/stock/net] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/object] Size=36 Context=Stock Type=Threshold [36x36@2/stock/object] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/table] Size=36 Context=Stock Type=Threshold [36x36@2/stock/table] Size=36 Scale=2 Context=Stock Type=Threshold [36x36/stock/text] Size=36 Context=Stock Type=Threshold [36x36@2/stock/text] Size=36 Scale=2 Context=Stock Type=Threshold [48x48/actions] Size=48 Context=Actions Type=Threshold [48x48@2/actions] Size=48 Scale=2 Context=Actions Type=Threshold [48x48/animations] Size=48 Context=Animations Type=Threshold [48x48@2/animations] Size=48 Scale=2 Context=Animations Type=Threshold [48x48/apps] Size=48 Context=Applications Type=Threshold [48x48@2/apps] Size=48 Scale=2 Context=Applications Type=Threshold [48x48/categories] Size=48 Context=Categories Type=Threshold [48x48@2/categories] Size=48 Scale=2 Context=Categories Type=Threshold [48x48/devices] Size=48 Context=Devices Type=Threshold [48x48@2/devices] Size=48 Scale=2 Context=Devices Type=Threshold [48x48/emblems] Size=48 Context=Emblems Type=Threshold [48x48@2/emblems] Size=48 Scale=2 Context=Emblems Type=Threshold [48x48/emotes] Size=48 Context=Emotes Type=Threshold [48x48@2/emotes] Size=48 Scale=2 Context=Emotes Type=Threshold [48x48/filesystems] Size=48 Context=FileSystems Type=Threshold [48x48@2/filesystems] Size=48 Scale=2 Context=FileSystems Type=Threshold [48x48/intl] Size=48 Context=International Type=Threshold [48x48@2/intl] Size=48 Scale=2 Context=International Type=Threshold [48x48/mimetypes] Size=48 Context=MimeTypes Type=Threshold [48x48@2/mimetypes] Size=48 Scale=2 Context=MimeTypes Type=Threshold [48x48/places] Size=48 Context=Places Type=Threshold [48x48@2/places] Size=48 Scale=2 Context=Places Type=Threshold [48x48/status] Size=48 Context=Status Type=Threshold [48x48@2/status] Size=48 Scale=2 Context=Status Type=Threshold [48x48/stock/chart] Size=48 Context=Stock Type=Threshold [48x48@2/stock/chart] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/code] Size=48 Context=Stock Type=Threshold [48x48@2/stock/code] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/data] Size=48 Context=Stock Type=Threshold [48x48@2/stock/data] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/form] Size=48 Context=Stock Type=Threshold [48x48@2/stock/form] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/image] Size=48 Context=Stock Type=Threshold [48x48@2/stock/image] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/io] Size=48 Context=Stock Type=Threshold [48x48@2/stock/io] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/media] Size=48 Context=Stock Type=Threshold [48x48@2/stock/media] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/navigation] Size=48 Context=Stock Type=Threshold [48x48@2/stock/navigation] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/net] Size=48 Context=Stock Type=Threshold [48x48@2/stock/net] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/object] Size=48 Context=Stock Type=Threshold [48x48@2/stock/object] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/table] Size=48 Context=Stock Type=Threshold [48x48@2/stock/table] Size=48 Scale=2 Context=Stock Type=Threshold [48x48/stock/text] Size=48 Context=Stock Type=Threshold [48x48@2/stock/text] Size=48 Scale=2 Context=Stock Type=Threshold [64x64/actions] Size=64 Context=Actions Type=Threshold [64x64@2/actions] Size=64 Scale=2 Context=Actions Type=Threshold [64x64/animations] Size=64 Context=Animations Type=Threshold [64x64@2/animations] Size=64 Scale=2 Context=Animations Type=Threshold [64x64/apps] Size=64 Context=Applications Type=Threshold [64x64@2/apps] Size=64 Scale=2 Context=Applications Type=Threshold [64x64/categories] Size=64 Context=Categories Type=Threshold [64x64@2/categories] Size=64 Scale=2 Context=Categories Type=Threshold [64x64/devices] Size=64 Context=Devices Type=Threshold [64x64@2/devices] Size=64 Scale=2 Context=Devices Type=Threshold [64x64/emblems] Size=64 Context=Emblems Type=Threshold [64x64@2/emblems] Size=64 Scale=2 Context=Emblems Type=Threshold [64x64/emotes] Size=64 Context=Emotes Type=Threshold [64x64@2/emotes] Size=64 Scale=2 Context=Emotes Type=Threshold [64x64/filesystems] Size=64 Context=FileSystems Type=Threshold [64x64@2/filesystems] Size=64 Scale=2 Context=FileSystems Type=Threshold [64x64/intl] Size=64 Context=International Type=Threshold [64x64@2/intl] Size=64 Scale=2 Context=International Type=Threshold [64x64/mimetypes] Size=64 Context=MimeTypes Type=Threshold [64x64@2/mimetypes] Size=64 Scale=2 Context=MimeTypes Type=Threshold [64x64/places] Size=64 Context=Places Type=Threshold [64x64@2/places] Size=64 Scale=2 Context=Places Type=Threshold [64x64/status] Size=64 Context=Status Type=Threshold [64x64@2/status] Size=64 Scale=2 Context=Status Type=Threshold [64x64/stock/chart] Size=64 Context=Stock Type=Threshold [64x64@2/stock/chart] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/code] Size=64 Context=Stock Type=Threshold [64x64@2/stock/code] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/data] Size=64 Context=Stock Type=Threshold [64x64@2/stock/data] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/form] Size=64 Context=Stock Type=Threshold [64x64@2/stock/form] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/image] Size=64 Context=Stock Type=Threshold [64x64@2/stock/image] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/io] Size=64 Context=Stock Type=Threshold [64x64@2/stock/io] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/media] Size=64 Context=Stock Type=Threshold [64x64@2/stock/media] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/navigation] Size=64 Context=Stock Type=Threshold [64x64@2/stock/navigation] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/net] Size=64 Context=Stock Type=Threshold [64x64@2/stock/net] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/object] Size=64 Context=Stock Type=Threshold [64x64@2/stock/object] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/table] Size=64 Context=Stock Type=Threshold [64x64@2/stock/table] Size=64 Scale=2 Context=Stock Type=Threshold [64x64/stock/text] Size=64 Context=Stock Type=Threshold [64x64@2/stock/text] Size=64 Scale=2 Context=Stock Type=Threshold [72x72/actions] Size=72 Context=Actions Type=Threshold [72x72@2/actions] Size=72 Scale=2 Context=Actions Type=Threshold [72x72/animations] Size=72 Context=Animations Type=Threshold [72x72@2/animations] Size=72 Scale=2 Context=Animations Type=Threshold [72x72/apps] Size=72 Context=Applications Type=Threshold [72x72@2/apps] Size=72 Scale=2 Context=Applications Type=Threshold [72x72/categories] Size=72 Context=Categories Type=Threshold [72x72@2/categories] Size=72 Scale=2 Context=Categories Type=Threshold [72x72/devices] Size=72 Context=Devices Type=Threshold [72x72@2/devices] Size=72 Scale=2 Context=Devices Type=Threshold [72x72/emblems] Size=72 Context=Emblems Type=Threshold [72x72@2/emblems] Size=72 Scale=2 Context=Emblems Type=Threshold [72x72/emotes] Size=72 Context=Emotes Type=Threshold [72x72@2/emotes] Size=72 Scale=2 Context=Emotes Type=Threshold [72x72/filesystems] Size=72 Context=FileSystems Type=Threshold [72x72@2/filesystems] Size=72 Scale=2 Context=FileSystems Type=Threshold [72x72/intl] Size=72 Context=International Type=Threshold [72x72@2/intl] Size=72 Scale=2 Context=International Type=Threshold [72x72/mimetypes] Size=72 Context=MimeTypes Type=Threshold [72x72@2/mimetypes] Size=72 Scale=2 Context=MimeTypes Type=Threshold [72x72/places] Size=72 Context=Places Type=Threshold [72x72@2/places] Size=72 Scale=2 Context=Places Type=Threshold [72x72/status] Size=72 Context=Status Type=Threshold [72x72@2/status] Size=72 Scale=2 Context=Status Type=Threshold [72x72/stock/chart] Size=72 Context=Stock Type=Threshold [72x72@2/stock/chart] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/code] Size=72 Context=Stock Type=Threshold [72x72@2/stock/code] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/data] Size=72 Context=Stock Type=Threshold [72x72@2/stock/data] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/form] Size=72 Context=Stock Type=Threshold [72x72@2/stock/form] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/image] Size=72 Context=Stock Type=Threshold [72x72@2/stock/image] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/io] Size=72 Context=Stock Type=Threshold [72x72@2/stock/io] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/media] Size=72 Context=Stock Type=Threshold [72x72@2/stock/media] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/navigation] Size=72 Context=Stock Type=Threshold [72x72@2/stock/navigation] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/net] Size=72 Context=Stock Type=Threshold [72x72@2/stock/net] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/object] Size=72 Context=Stock Type=Threshold [72x72@2/stock/object] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/table] Size=72 Context=Stock Type=Threshold [72x72@2/stock/table] Size=72 Scale=2 Context=Stock Type=Threshold [72x72/stock/text] Size=72 Context=Stock Type=Threshold [72x72@2/stock/text] Size=72 Scale=2 Context=Stock Type=Threshold [96x96/actions] Size=96 Context=Actions Type=Threshold [96x96@2/actions] Size=96 Scale=2 Context=Actions Type=Threshold [96x96/animations] Size=96 Context=Animations Type=Threshold [96x96@2/animations] Size=96 Scale=2 Context=Animations Type=Threshold [96x96/apps] Size=96 Context=Applications Type=Threshold [96x96@2/apps] Size=96 Scale=2 Context=Applications Type=Threshold [96x96/categories] Size=96 Context=Categories Type=Threshold [96x96@2/categories] Size=96 Scale=2 Context=Categories Type=Threshold [96x96/devices] Size=96 Context=Devices Type=Threshold [96x96@2/devices] Size=96 Scale=2 Context=Devices Type=Threshold [96x96/emblems] Size=96 Context=Emblems Type=Threshold [96x96@2/emblems] Size=96 Scale=2 Context=Emblems Type=Threshold [96x96/emotes] Size=96 Context=Emotes Type=Threshold [96x96@2/emotes] Size=96 Scale=2 Context=Emotes Type=Threshold [96x96/filesystems] Size=96 Context=FileSystems Type=Threshold [96x96@2/filesystems] Size=96 Scale=2 Context=FileSystems Type=Threshold [96x96/intl] Size=96 Context=International Type=Threshold [96x96@2/intl] Size=96 Scale=2 Context=International Type=Threshold [96x96/mimetypes] Size=96 Context=MimeTypes Type=Threshold [96x96@2/mimetypes] Size=96 Scale=2 Context=MimeTypes Type=Threshold [96x96/places] Size=96 Context=Places Type=Threshold [96x96@2/places] Size=96 Scale=2 Context=Places Type=Threshold [96x96/status] Size=96 Context=Status Type=Threshold [96x96@2/status] Size=96 Scale=2 Context=Status Type=Threshold [96x96/stock/chart] Size=96 Context=Stock Type=Threshold [96x96@2/stock/chart] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/code] Size=96 Context=Stock Type=Threshold [96x96@2/stock/code] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/data] Size=96 Context=Stock Type=Threshold [96x96@2/stock/data] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/form] Size=96 Context=Stock Type=Threshold [96x96@2/stock/form] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/image] Size=96 Context=Stock Type=Threshold [96x96@2/stock/image] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/io] Size=96 Context=Stock Type=Threshold [96x96@2/stock/io] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/media] Size=96 Context=Stock Type=Threshold [96x96@2/stock/media] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/navigation] Size=96 Context=Stock Type=Threshold [96x96@2/stock/navigation] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/net] Size=96 Context=Stock Type=Threshold [96x96@2/stock/net] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/object] Size=96 Context=Stock Type=Threshold [96x96@2/stock/object] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/table] Size=96 Context=Stock Type=Threshold [96x96@2/stock/table] Size=96 Scale=2 Context=Stock Type=Threshold [96x96/stock/text] Size=96 Context=Stock Type=Threshold [96x96@2/stock/text] Size=96 Scale=2 Context=Stock Type=Threshold [128x128/actions] Size=128 Context=Actions Type=Threshold [128x128@2/actions] Size=128 Scale=2 Context=Actions Type=Threshold [128x128/animations] Size=128 Context=Animations Type=Threshold [128x128@2/animations] Size=128 Scale=2 Context=Animations Type=Threshold [128x128/apps] Size=128 Context=Applications Type=Threshold [128x128@2/apps] Size=128 Scale=2 Context=Applications Type=Threshold [128x128/categories] Size=128 Context=Categories Type=Threshold [128x128@2/categories] Size=128 Scale=2 Context=Categories Type=Threshold [128x128/devices] Size=128 Context=Devices Type=Threshold [128x128@2/devices] Size=128 Scale=2 Context=Devices Type=Threshold [128x128/emblems] Size=128 Context=Emblems Type=Threshold [128x128@2/emblems] Size=128 Scale=2 Context=Emblems Type=Threshold [128x128/emotes] Size=128 Context=Emotes Type=Threshold [128x128@2/emotes] Size=128 Scale=2 Context=Emotes Type=Threshold [128x128/filesystems] Size=128 Context=FileSystems Type=Threshold [128x128@2/filesystems] Size=128 Scale=2 Context=FileSystems Type=Threshold [128x128/intl] Size=128 Context=International Type=Threshold [128x128@2/intl] Size=128 Scale=2 Context=International Type=Threshold [128x128/mimetypes] Size=128 Context=MimeTypes Type=Threshold [128x128@2/mimetypes] Size=128 Scale=2 Context=MimeTypes Type=Threshold [128x128/places] Size=128 Context=Places Type=Threshold [128x128@2/places] Size=128 Scale=2 Context=Places Type=Threshold [128x128/status] Size=128 Context=Status Type=Threshold [128x128@2/status] Size=128 Scale=2 Context=Status Type=Threshold [128x128/stock/chart] Size=128 Context=Stock Type=Threshold [128x128@2/stock/chart] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/code] Size=128 Context=Stock Type=Threshold [128x128@2/stock/code] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/data] Size=128 Context=Stock Type=Threshold [128x128@2/stock/data] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/form] Size=128 Context=Stock Type=Threshold [128x128@2/stock/form] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/image] Size=128 Context=Stock Type=Threshold [128x128@2/stock/image] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/io] Size=128 Context=Stock Type=Threshold [128x128@2/stock/io] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/media] Size=128 Context=Stock Type=Threshold [128x128@2/stock/media] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/navigation] Size=128 Context=Stock Type=Threshold [128x128@2/stock/navigation] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/net] Size=128 Context=Stock Type=Threshold [128x128@2/stock/net] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/object] Size=128 Context=Stock Type=Threshold [128x128@2/stock/object] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/table] Size=128 Context=Stock Type=Threshold [128x128@2/stock/table] Size=128 Scale=2 Context=Stock Type=Threshold [128x128/stock/text] Size=128 Context=Stock Type=Threshold [128x128@2/stock/text] Size=128 Scale=2 Context=Stock Type=Threshold [192x192/actions] Size=192 Context=Actions Type=Threshold [192x192@2/actions] Size=192 Scale=2 Context=Actions Type=Threshold [192x192/animations] Size=192 Context=Animations Type=Threshold [192x192@2/animations] Size=192 Scale=2 Context=Animations Type=Threshold [192x192/apps] Size=192 Context=Applications Type=Threshold [192x192@2/apps] Size=192 Scale=2 Context=Applications Type=Threshold [192x192/categories] Size=192 Context=Categories Type=Threshold [192x192@2/categories] Size=192 Scale=2 Context=Categories Type=Threshold [192x192/devices] Size=192 Context=Devices Type=Threshold [192x192@2/devices] Size=192 Scale=2 Context=Devices Type=Threshold [192x192/emblems] Size=192 Context=Emblems Type=Threshold [192x192@2/emblems] Size=192 Scale=2 Context=Emblems Type=Threshold [192x192/emotes] Size=192 Context=Emotes Type=Threshold [192x192@2/emotes] Size=192 Scale=2 Context=Emotes Type=Threshold [192x192/filesystems] Size=192 Context=FileSystems Type=Threshold [192x192@2/filesystems] Size=192 Scale=2 Context=FileSystems Type=Threshold [192x192/intl] Size=192 Context=International Type=Threshold [192x192@2/intl] Size=192 Scale=2 Context=International Type=Threshold [192x192/mimetypes] Size=192 Context=MimeTypes Type=Threshold [192x192@2/mimetypes] Size=192 Scale=2 Context=MimeTypes Type=Threshold [192x192/places] Size=192 Context=Places Type=Threshold [192x192@2/places] Size=192 Scale=2 Context=Places Type=Threshold [192x192/status] Size=192 Context=Status Type=Threshold [192x192@2/status] Size=192 Scale=2 Context=Status Type=Threshold [192x192/stock/chart] Size=192 Context=Stock Type=Threshold [192x192@2/stock/chart] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/code] Size=192 Context=Stock Type=Threshold [192x192@2/stock/code] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/data] Size=192 Context=Stock Type=Threshold [192x192@2/stock/data] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/form] Size=192 Context=Stock Type=Threshold [192x192@2/stock/form] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/image] Size=192 Context=Stock Type=Threshold [192x192@2/stock/image] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/io] Size=192 Context=Stock Type=Threshold [192x192@2/stock/io] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/media] Size=192 Context=Stock Type=Threshold [192x192@2/stock/media] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/navigation] Size=192 Context=Stock Type=Threshold [192x192@2/stock/navigation] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/net] Size=192 Context=Stock Type=Threshold [192x192@2/stock/net] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/object] Size=192 Context=Stock Type=Threshold [192x192@2/stock/object] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/table] Size=192 Context=Stock Type=Threshold [192x192@2/stock/table] Size=192 Scale=2 Context=Stock Type=Threshold [192x192/stock/text] Size=192 Context=Stock Type=Threshold [192x192@2/stock/text] Size=192 Scale=2 Context=Stock Type=Threshold [256x256/actions] MinSize=64 Size=256 MaxSize=256 Context=Actions Type=Scalable [256x256@2/actions] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Actions Type=Scalable [256x256/animations] MinSize=64 Size=256 MaxSize=256 Context=Animations Type=Scalable [256x256@2/animations] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Animations Type=Scalable [256x256/apps] MinSize=64 Size=256 MaxSize=256 Context=Applications Type=Scalable [256x256@2/apps] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Applications Type=Scalable [256x256/categories] MinSize=64 Size=256 MaxSize=256 Context=Categories Type=Scalable [256x256@2/categories] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Categories Type=Scalable [256x256/devices] MinSize=64 Size=256 MaxSize=256 Context=Devices Type=Scalable [256x256@2/devices] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Devices Type=Scalable [256x256/emblems] MinSize=64 Size=256 MaxSize=256 Context=Emblems Type=Scalable [256x256@2/emblems] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Emblems Type=Scalable [256x256/emotes] MinSize=64 Size=256 MaxSize=256 Context=Emotes Type=Scalable [256x256@2/emotes] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Emotes Type=Scalable [256x256/filesystems] MinSize=64 Size=256 MaxSize=256 Context=FileSystems Type=Scalable [256x256@2/filesystems] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=FileSystems Type=Scalable [256x256/intl] MinSize=64 Size=256 MaxSize=256 Context=International Type=Scalable [256x256@2/intl] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=International Type=Scalable [256x256/mimetypes] MinSize=64 Size=256 MaxSize=256 Context=MimeTypes Type=Scalable [256x256@2/mimetypes] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=MimeTypes Type=Scalable [256x256/places] MinSize=64 Size=256 MaxSize=256 Context=Places Type=Scalable [256x256@2/places] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Places Type=Scalable [256x256/status] MinSize=64 Size=256 MaxSize=256 Context=Status Type=Scalable [256x256@2/status] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Status Type=Scalable [256x256/stock/chart] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/chart] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/code] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/code] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/data] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/data] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/form] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/form] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/image] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/image] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/io] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/io] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/media] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/media] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/navigation] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/navigation] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/net] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/net] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/object] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/object] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/table] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/table] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [256x256/stock/text] MinSize=64 Size=256 MaxSize=256 Context=Stock Type=Scalable [256x256@2/stock/text] MinSize=64 Size=256 Scale=2 MaxSize=256 Context=Stock Type=Scalable [512x512/actions] MinSize=64 Size=512 MaxSize=512 Context=Actions Type=Scalable [512x512@2/actions] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Actions Type=Scalable [512x512/animations] MinSize=64 Size=512 MaxSize=512 Context=Animations Type=Scalable [512x512@2/animations] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Animations Type=Scalable [512x512/apps] MinSize=64 Size=512 MaxSize=512 Context=Applications Type=Scalable [512x512@2/apps] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Applications Type=Scalable [512x512/categories] MinSize=64 Size=512 MaxSize=512 Context=Categories Type=Scalable [512x512@2/categories] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Categories Type=Scalable [512x512/devices] MinSize=64 Size=512 MaxSize=512 Context=Devices Type=Scalable [512x512@2/devices] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Devices Type=Scalable [512x512/emblems] MinSize=64 Size=512 MaxSize=512 Context=Emblems Type=Scalable [512x512@2/emblems] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Emblems Type=Scalable [512x512/emotes] MinSize=64 Size=512 MaxSize=512 Context=Emotes Type=Scalable [512x512@2/emotes] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Emotes Type=Scalable [512x512/filesystems] MinSize=64 Size=512 MaxSize=512 Context=FileSystems Type=Scalable [512x512@2/filesystems] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=FileSystems Type=Scalable [512x512/intl] MinSize=64 Size=512 MaxSize=512 Context=International Type=Scalable [512x512@2/intl] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=International Type=Scalable [512x512/mimetypes] MinSize=64 Size=512 MaxSize=512 Context=MimeTypes Type=Scalable [512x512@2/mimetypes] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=MimeTypes Type=Scalable [512x512/places] MinSize=64 Size=512 MaxSize=512 Context=Places Type=Scalable [512x512@2/places] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Places Type=Scalable [512x512/status] MinSize=64 Size=512 MaxSize=512 Context=Status Type=Scalable [512x512@2/status] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Status Type=Scalable [512x512/stock/chart] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/chart] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/code] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/code] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/data] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/data] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/form] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/form] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/image] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/image] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/io] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/io] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/media] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/media] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/navigation] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/navigation] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/net] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/net] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/object] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/object] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/table] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/table] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [512x512/stock/text] MinSize=64 Size=512 MaxSize=512 Context=Stock Type=Scalable [512x512@2/stock/text] MinSize=64 Size=512 Scale=2 MaxSize=512 Context=Stock Type=Scalable [scalable/actions] MinSize=1 Size=128 MaxSize=256 Context=Actions Type=Scalable [scalable/animations] MinSize=1 Size=128 MaxSize=256 Context=Animations Type=Scalable [scalable/apps] MinSize=1 Size=128 MaxSize=256 Context=Applications Type=Scalable [scalable/categories] MinSize=1 Size=128 MaxSize=256 Context=Categories Type=Scalable [scalable/devices] MinSize=1 Size=128 MaxSize=512 Context=Devices Type=Scalable [scalable/emblems] MinSize=1 Size=128 MaxSize=256 Context=Emblems Type=Scalable [scalable/emotes] MinSize=1 Size=128 MaxSize=512 Context=Emotes Type=Scalable [scalable/filesystems] MinSize=1 Size=128 MaxSize=256 Context=FileSystems Type=Scalable [scalable/intl] MinSize=1 Size=128 MaxSize=512 Context=International Type=Scalable [scalable/mimetypes] MinSize=1 Size=128 MaxSize=256 Context=MimeTypes Type=Scalable [scalable/places] MinSize=1 Size=128 MaxSize=512 Context=Places Type=Scalable [scalable/status] MinSize=1 Size=128 MaxSize=256 Context=Status Type=Scalable [scalable/stock/chart] MinSize=1 Size=128 MaxSize=512 Context=Stock Type=Scalable [scalable/stock/code] MinSize=1 Size=128 MaxSize=256 Context=Stock Type=Scalable [scalable/stock/data] MinSize=1 Size=128 MaxSize=512 Context=Stock Type=Scalable [scalable/stock/form] MinSize=1 Size=128 MaxSize=256 Context=Stock Type=Scalable [scalable/stock/image] MinSize=1 Size=128 MaxSize=512 Context=Stock Type=Scalable [scalable/stock/io] MinSize=1 Size=128 MaxSize=256 Context=Stock Type=Scalable [scalable/stock/media] MinSize=1 Size=128 MaxSize=512 Context=Stock Type=Scalable [scalable/stock/navigation] MinSize=1 Size=128 MaxSize=256 Context=Stock Type=Scalable [scalable/stock/net] MinSize=1 Size=128 MaxSize=512 Context=Stock Type=Scalable [scalable/stock/object] MinSize=1 Size=128 MaxSize=256 Context=Stock Type=Scalable [scalable/stock/table] MinSize=1 Size=128 MaxSize=512 Context=Stock Type=Scalable [scalable/stock/text] MinSize=1 Size=128 MaxSize=256 Context=Stock Type=Scalable [symbolic/apps] MinSize=8 Size=16 MaxSize=512 Context=Applications Type=Scalable appstream-generator-0.10.1/data/meson.build000066400000000000000000000020051506754475600206440ustar00rootroot00000000000000 # data install_data('asgen-hints.json', install_dir: 'share/appstream') install_data('hicolor-theme-index.theme', install_dir: 'share/appstream') ascli_exe = find_program('appstreamcli', required: true) metainfo_filename = 'org.freedesktop.appstream.generator.metainfo.xml' metainfo_with_relinfo = custom_target('add-metainfo-releases', input : ['../NEWS', files(metainfo_filename)], output : [metainfo_filename], command : [ascli_exe, 'news-to-metainfo', '--limit=6', '@INPUT0@', '@INPUT1@', '@OUTPUT@'], install : true, install_dir : join_paths (get_option ('datadir'), 'metainfo') ) if ascli_exe.found() # Validate our MetaInfo file test('asgen-validate_metainfo', ascli_exe, args: ['validate', '--no-net', '--pedantic', files(metainfo_filename)] ) endif # templates #install_subdir('data/templates/', install_dir: 'share/appstream') # FIXME: Doesn't handle dir symlinks correctly meson.add_install_script(source_root + '/contrib/setup/meson-install-templates.sh') appstream-generator-0.10.1/data/org.freedesktop.appstream.generator.metainfo.xml000066400000000000000000000037341506754475600301200ustar00rootroot00000000000000 org.freedesktop.appstream.generator FSFAP LGPL-3.0+ AppStream Generator A fast AppStream metadata generator

AppStream is a metadata specification which permits software components to provide information about themselves to automated systems and end-users before the software is actually installed.

The appstream-generator tool generates AppStream metadata from the repositories of a software distribution. It currently supports the following repository formats / distributions: Debian, Ubuntu, Arch Linux, RPM-MD (Fedora, Mageia).

The generator will produce AppStream catalog metadata files in the AppStream YAML or XML format to be shipped to users, as well as a detailed HTML report about found components and HTML and JSON reports on issues detected with the scanned metadata. It reads .desktop files as well as metainfo files, renders fonts, scales images, caches screenshots etc. to produce high-quality metadata for AppStream based software centers to consume. Usually, appstream-generator is integrated with the existing software build & packaging workflow of a distribution.

https://github.com/ximion/appstream-generator https://github.com/ximion/appstream-generator/blob/master/docs/index.md https://github.com/ximion/appstream-generator/issues Freedesktop Matthias Klumpp appstream-generator
appstream-generator-0.10.1/data/templates/000077500000000000000000000000001506754475600205035ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/debian/000077500000000000000000000000001506754475600217255ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/debian/base.html000077700000000000000000000000001506754475600271572../default/base.htmlustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/debian/main.html000066400000000000000000000062711506754475600235450ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Start{% endblock %} {% block header_content %} AppStream data hints for {{project_name}} {% endblock %} {% block content %}

Welcome!

Welcome to the AppStream Generator HTML pages!

These pages exist to provide a user-friendly view on the issues discovered by the AppStream metadata generator while extracting metadata from packages in the {{project_name}} archive. They can also be used to take a look at the raw metadata, to spot possible problems with the data itself or the generation process.

Select a suite

{% for suite in suites %}

{{suite.suite}}

{% endfor %}
{% for oldsuite in oldsuites %}

{{oldsuite.suite}}

{% endfor %}

AppStream Generator Logo

What is AppStream and DEP-11?

AppStream is a cross-distro XML format to provide metadata for software components and to assign unique identifiers to software.
In Debian, we parse all XML provided by upstream projects as well as other metadata (.desktop-files, ...), and compile a single YAML metadata file from it, which is then shipped to users via APT.

While the official AppStream specification is based on XML, Debian uses a YAML version of the format for easier use in existing scripts and for better archive integration. This format is called DEP-11, and initially had a much wider scope in enhancing archive metadata than AppStream had. Today AppStream covers that as well, and DEP-11 is only a YAML version of AppStream.

The generated metadata can for example be used by software centers like GNOME Software or KDE Discover to display a user-friendly application-centric view on the package archive.
It can also be used by other software to find missing plugins, codecs, fonts, etc. or simply by users to install software on any Linux distribution without knowing the exact package name.

More information

See AppStream @ wiki.d.o for information about AppStream integration and usage in Debian.
The offical AppStream specification can be found at freedesktop.org, a description of the DEP-11 YAML format is hosted there as well.

You can find the source-code of the AppStream Generator here.

Log files of the generator runs are stored in logs/, machine-readable issue hints can be found in hints/, valid metadata is located in data/ and all exported media is made available via media/.

{% endblock %} appstream-generator-0.10.1/data/templates/debian/static/000077500000000000000000000000001506754475600232145ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/debian/static/css/000077500000000000000000000000001506754475600240045ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/debian/static/css/highlight.css000077700000000000000000000000001506754475600355052../../../default/static/css/highlight.cssustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/debian/static/css/style.css000066400000000000000000000075331506754475600256660ustar00rootroot00000000000000html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust:100%; height:100%; } body { border-sizing: border-box; font-family: Cantarell,"Helvetica Neue",Helvetica,Arial,sans-serif; font-size: 14px; line-height: 20px; color: #333333; margin: 0; height: 93%; } a { color: #337ab7; text-decoration: none; background-color: transparent; } .headbar { border-radius: 4px; display: block; border: 1px solid transparent; margin-bottom: 20px; min-height: 50px; position: relative; background-color: #f8f8f8; border-color: #e7e7e7; border-width: 0 0 1px; z-index: 1000; border-radius: 0; margin-bottom: 14px; } .headbar-content { font-size: 18px; line-height: 20px; padding: 15px; float: left; } .headbar-content-right { font-size: 14px; line-height: 20px; padding: 15px; float: right; } .content { padding: 0em 1em 0em 1em; } .wrapper { width: 60%; } .wrapper hr { border-top: none; border-bottom: 1px solid #819eb7; margin-bottom: 1em; } img.fit { max-width: 99%; max-height: 99%; } hr { border-top: none; border-bottom: 1px solid #d70a53; margin-bottom: 1em; } footer { text-align: center; margin-top: 1em; } span.avoidwrap { display: inline-block; } .infobox { border-color: #eee; border-image: none; border-radius: 3px; border-style: solid; border-width: 1px 1px 1px 5px; margin: 20px 0; padding: 20px; } .infobox h2 { margin-bottom: 5px; margin-top: 0; } .infobox p:last-child { margin-bottom: 0 } .infobox-hint { border-left-color: #1b809e; } .infobox-hint h2 { color: #1b809e; } .infobox-error { border-left-color: #ce4844; } .infobox-error h2 { color: #ce4844; } .infobox-warning { border-left-color: #aa6708; } .infobox-warning h2 { color: #aa6708; } /* label styles copied from Bootstrap */ .label { border-radius: 0.25em; color: #fff; display: inline; font-size: 75%; font-weight: 700; line-height: 1; padding: 0.2em 0.6em 0.3em; text-align: center; vertical-align: baseline; white-space: nowrap; } .label-info { background-color: #5bc0de; } .label-warning { background-color: #f0ad4e; } .label-error { background-color: #d9534f; } .label-neutral { background-color: #777; } .overviewlisting a { color: #000000; text-decoration: none; } .overviewlisting li { padding: 2px 4px 2px; } code { background-color: #f9f2f4; border-radius: 4px; color: #c7254e; font-size: 90%; padding: 2px 4px; } .well{ min-height:20px; padding:19px; margin-bottom:20px; background-color:#f5f5f5; border:1px solid #e3e3e3; border-radius:4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05); box-shadow:inset 0 1px 1px rgba(0,0,0,.05) } .progress { background-color: #f5f5f5; border-radius: 4px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset; height: 20px; margin-bottom: 20px; overflow: hidden; } .progress-bar { background-color: #337ab7; box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15) inset; color: #fff; float: left; font-size: 12px; height: 100%; line-height: 20px; text-align: center; transition: width 0.6s ease 0s; width: 0; } .progress-bar-blue { background-color: #5bc0de; } .progress-bar-green { background-color: #5cb85c; } .progress-bar-yellow { background-color: #f0ad4e; } .progress-bar-red { background-color: #d9534f; } .sr-only { border: 0 none; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } .permalink { font-size: 75%; color: #999; line-height: 100%; font-weight: normal; text-decoration: none; } appstream-generator-0.10.1/data/templates/debian/static/img000077700000000000000000000000001506754475600300252../../default/static/img/ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/debian/static/js000077700000000000000000000000001506754475600274462../../default/static/jsustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/default/000077500000000000000000000000001506754475600221275ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/default/base.html000066400000000000000000000021421506754475600237260ustar00rootroot00000000000000 AppStream Report for {{project_name}} - {% block title %}{% endblock %} {% block head_extra %}{% endblock %}
{% block header_content %}{% endblock %}
{% block float_right %}{% endblock %}
{% block content %}{% endblock %}

Generated by appstream-generator (v{{generator_version}}).

{% block page_details %}{% endblock %}
appstream-generator-0.10.1/data/templates/default/issues_index.html000066400000000000000000000024121506754475600255160ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Hints summary for {{suite}}/{{section}}{% endblock %} {% block header_content %} ⇦ | Hints summary for {{suite}}/{{section}} {% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} {% block content %}

Metadata processing hints found for {{suite}}/{{section}}

{% for summary in summaries %}

{{summary.maintainer}}

    {% for package in summary.packages %}
  • {{package.pkgname}}  {% if package.info_count > 0 %} Infos: {{package.info_count}} {% endif %} {% if package.warning_count > 0 %} Warnings: {{package.warning_count}} {% endif %} {% if package.error_count > 0 %} Errors: {{package.error_count}} {% endif %}
  • {% endfor %}
{% endfor %} {% endblock %} appstream-generator-0.10.1/data/templates/default/issues_page.html000066400000000000000000000034641506754475600253330ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Issues for {{package_name}} in {{suite}}/{{section}}{% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} {% block header_content %} ⇦ | {{package_name}} [{{section}}] {% endblock %} {% block content %}

Hints for {{package_name}} in {{section}}

{% for entry in entries %}

{{entry.component_id}} {% for arch in entry.architectures %} ⚙ {{arch.arch}} {% endfor %}

{% if entry.has_errors %}

Errors

    {% for error in entry.errors %}
  • {{error.error_tag}}
    {{error.error_description}}
  • {% endfor %}
{% endif %} {% if entry.has_warnings %}

Warnings

    {% for warning in entry.warnings %}
  • {{warning.warning_tag}}
    {{warning.warning_description}}
  • {% endfor %}
{% endif %} {% if entry.has_infos %}

Hints

    {% for info in entry.infos %}
  • {{info.info_tag}}
    {{info.info_description}}
  • {% endfor %}
{% endif %} {% endfor %}
{% endblock %} appstream-generator-0.10.1/data/templates/default/main.html000066400000000000000000000042511506754475600237430ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Start{% endblock %} {% block header_content %} AppStream data hints for {{project_name}} {% endblock %} {% block content %}

Welcome!

Welcome to the AppStream Generator HTML pages!

These pages exist to provide a user-friendly view on the issues discovered by the AppStream metadata generator while extracting metadata from packages in the {{project_name}} archive. They can also be used to take a look at the raw metadata, to spot possible problems with the data itself or the generation process.

Select a suite

{% for suite in suites %}

{{suite.suite}}

{% endfor %}
{% for oldsuite in oldsuites %}

{{oldsuite.suite}}

{% endfor %}

AppStream Generator Logo

What is AppStream?

AppStream is a cross-distro XML format to provide metadata for software components and to assign unique identifiers to software.
In {{project_name}}, we parse all XML provided by upstream projects as well as other metadata (.desktop-files, ...), and compile a single metadata file from it, which is then shipped to users.

The generated metadata can for example be used by software centers like GNOME Software or KDE Discover to display a user-friendly application-centric view on the package archive.
It can also be used by other software to find missing plugins, codecs, fonts, etc. or simply by users to install software on any Linux distribution without knowing the exact package name.

More information

The offical AppStream specification can be found at freedesktop.org.

You can find the source-code of the AppStream Generator here.

{% endblock %} appstream-generator-0.10.1/data/templates/default/metainfo_index.html000066400000000000000000000017501506754475600260110ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Components in {{suite}}/{{section}}{% endblock %} {% block header_content %} ⇦ | Components summary for {{suite}}/{{section}} {% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} {% block content %}

Metadata for {{suite}}/{{section}}

{% for summary in summaries %}

{{summary.maintainer}}

    {% for package in summary.packages %}
  • {{package.pkgname}} 
      {% for component in package.components %}
    • {{component.cid}}
    • {% endfor %}
  • {% endfor %}
{% endfor %} {% endblock %} appstream-generator-0.10.1/data/templates/default/metainfo_page.html000066400000000000000000000021761506754475600256210ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}{{package_name}} in {{suite}}/{{section}}{% endblock %} {% block head_extra %} {% endblock %} {% block header_content %} ⇦ | {{package_name}} [{{section}}] {% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} {% block content %}

Metadata for {{package_name}} in {{section}}

{% for cpt in cpts %}

{{cpt.component_id}} {% for arch in cpt.architectures %} ⚙ {{arch.arch}} {% endfor %}

Icon
{{cpt.metadata}}
{% endfor %}
{% endblock %} appstream-generator-0.10.1/data/templates/default/section_page.html000066400000000000000000000133721506754475600254630ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Data for {{suite}}/{{section}}{% endblock %} {% block head_extra %} {% endblock %} {% block header_content %} ⇦ | AppStream data for {{project_name}}/{{suite}}/{{section}} {% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} {% block content %}

Overview for {{suite}}/{{section}}

Data

Issues - Issues found while extracting the data

Metainfo - Per-package view of the generated data

Health

Issue overview

{{valid_percentage}}% Valid
{{info_percentage}}% Infos
{{warning_percentage}}% Warnings
{{error_percentage}}% Errors
  • {{metainfo_count}} valid components
  • {{error_count}} errors
  • {{warning_count}} warnings
  • {{info_count}} infos/hints
{% endblock %} appstream-generator-0.10.1/data/templates/default/sections_index.html000066400000000000000000000071071506754475600260400ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Data for the {{suite}} suite{% endblock %} {% block header_content %} ⇦ | AppStream data for {{project_name}}/{{suite}} {% endblock %} {% block head_extra %} {% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} {% block content %}

Select an archive section

Sections

{% for section in sections %}

{{section.section}}

{% endfor %}

Health of suite "{{suite}}"

{% for section in sections %}
{{section.section}}
{% endfor %}
{% endblock %} appstream-generator-0.10.1/data/templates/default/static/000077500000000000000000000000001506754475600234165ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/default/static/css/000077500000000000000000000000001506754475600242065ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/default/static/css/highlight.css000066400000000000000000000021741506754475600266730ustar00rootroot00000000000000/* github.com style (c) Vasily Polovnyov */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #333; background: #f5f5f5; } .hljs-comment, .hljs-quote { color: #998; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #333; font-weight: bold; } .hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr { color: #008080; } .hljs-string, .hljs-doctag { color: #d14; } .hljs-title, .hljs-section, .hljs-selector-id { color: #900; font-weight: bold; } .hljs-subst { font-weight: normal; } .hljs-type, .hljs-class .hljs-title { color: #458; font-weight: bold; } .hljs-tag, .hljs-name, .hljs-attribute { color: #000080; font-weight: normal; } .hljs-regexp, .hljs-link { color: #009926; } .hljs-symbol, .hljs-bullet { color: #990073; } .hljs-built_in, .hljs-builtin-name { color: #0086b3; } .hljs-meta { color: #999; font-weight: bold; } .hljs-deletion { background: #fdd; } .hljs-addition { background: #dfd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } appstream-generator-0.10.1/data/templates/default/static/css/style.css000066400000000000000000000075331506754475600260700ustar00rootroot00000000000000html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust:100%; height:100%; } body { border-sizing: border-box; font-family: Cantarell,"Helvetica Neue",Helvetica,Arial,sans-serif; font-size: 14px; line-height: 20px; color: #333333; margin: 0; height: 93%; } a { color: #337ab7; text-decoration: none; background-color: transparent; } .headbar { border-radius: 4px; display: block; border: 1px solid transparent; margin-bottom: 20px; min-height: 50px; position: relative; background-color: #f8f8f8; border-color: #e7e7e7; border-width: 0 0 1px; z-index: 1000; border-radius: 0; margin-bottom: 14px; } .headbar-content { font-size: 18px; line-height: 20px; padding: 15px; float: left; } .headbar-content-right { font-size: 14px; line-height: 20px; padding: 15px; float: right; } .content { padding: 0em 1em 0em 1em; } .wrapper { width: 60%; } .wrapper hr { border-top: none; border-bottom: 1px solid #819eb7; margin-bottom: 1em; } img.fit { max-width: 99%; max-height: 99%; } hr { border-top: none; border-bottom: 1px solid #0a630d; margin-bottom: 1em; } footer { text-align: center; margin-top: 1em; } span.avoidwrap { display: inline-block; } .infobox { border-color: #eee; border-image: none; border-radius: 3px; border-style: solid; border-width: 1px 1px 1px 5px; margin: 20px 0; padding: 20px; } .infobox h2 { margin-bottom: 5px; margin-top: 0; } .infobox p:last-child { margin-bottom: 0 } .infobox-hint { border-left-color: #1b809e; } .infobox-hint h2 { color: #1b809e; } .infobox-error { border-left-color: #ce4844; } .infobox-error h2 { color: #ce4844; } .infobox-warning { border-left-color: #aa6708; } .infobox-warning h2 { color: #aa6708; } /* label styles copied from Bootstrap */ .label { border-radius: 0.25em; color: #fff; display: inline; font-size: 75%; font-weight: 700; line-height: 1; padding: 0.2em 0.6em 0.3em; text-align: center; vertical-align: baseline; white-space: nowrap; } .label-info { background-color: #5bc0de; } .label-warning { background-color: #f0ad4e; } .label-error { background-color: #d9534f; } .label-neutral { background-color: #777; } .overviewlisting a { color: #000000; text-decoration: none; } .overviewlisting li { padding: 2px 4px 2px; } code { background-color: #f9f2f4; border-radius: 4px; color: #c7254e; font-size: 90%; padding: 2px 4px; } .well{ min-height:20px; padding:19px; margin-bottom:20px; background-color:#f5f5f5; border:1px solid #e3e3e3; border-radius:4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05); box-shadow:inset 0 1px 1px rgba(0,0,0,.05) } .progress { background-color: #f5f5f5; border-radius: 4px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset; height: 20px; margin-bottom: 20px; overflow: hidden; } .progress-bar { background-color: #337ab7; box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15) inset; color: #fff; float: left; font-size: 12px; height: 100%; line-height: 20px; text-align: center; transition: width 0.6s ease 0s; width: 0; } .progress-bar-blue { background-color: #5bc0de; } .progress-bar-green { background-color: #5cb85c; } .progress-bar-yellow { background-color: #f0ad4e; } .progress-bar-red { background-color: #d9534f; } .sr-only { border: 0 none; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } .permalink { font-size: 75%; color: #999; line-height: 100%; font-weight: normal; text-decoration: none; } appstream-generator-0.10.1/data/templates/default/static/img/000077500000000000000000000000001506754475600241725ustar00rootroot00000000000000appstream-generator-0.10.1/data/templates/default/static/img/asgen.png000066400000000000000000000365031506754475600260040ustar00rootroot00000000000000PNG  IHDR sBIT|d pHYs tEXtSoftwarewww.inkscape.org<?ٔ >}ϟھww{*pc\ǹ9q2'x,ظyM~RW,ԕR~97zf"< sӝx 1uQ~'C~?oIYDIĺ u[o /8{RA-\>e{loӘO^k{J{1_!u Qyیk }O!y:O]Y}oڨ8{T==KnU"D ӭFtn?>==ܕl{{7zQb(gMԼqt@kP;L>$wmOUvh!ڱhѝhZzD!}=d%R.M4)ԘϵLS)(wy艴,j>cmzs9n=,N=~OfS(Q#iACRdD.A)`'oF}9~)c!k^P1#Qϴ Ϩ+RiY u=Nw$Z#+CR*ٵy A3\,Y^usm4<-@I]#ԜFK IS$Q37̀pr2x+"߆; /Q+~^OE3gH{ΜB[N/):Bt2/N>:pӈ1/Ԑw-ҏةSd N7F%zR,} 0Zz)&!8IOZsk60_xH{=)>qM5"jCL1rlk[WHJ.mV%6}5cGcV`MO&!CM}YWN8!ft3Hq年.fs1;=#TJM1W R4)F,>;oIJ8]W$`7H'N<,gK?:9,+RO-,~h+q>2O\ gΜyxر"nCǦp{BjՄ<:ΰ;ͯ 575ǎ} Ɂig5@Sn1S&Φ}-9tLNN.<2== @1BD/ etuOOMzP7E}7=!=:i\looނ?G F˻'gL.~hZ>sHh!$hg{_>zte;yA3=υf#{ԤBUS裩=hZrh'A^Y=S!e; m 0HMn}uBڹRx/}-sr$_%Lxdj5Tz:<5e~6[4MhZzdӃ<*AtD4{Vb١%BjH㓓MM!|zфǎ7Z \|s#?EH %!}]!(brV%Yvd ;#!}2Kgq<81gONU_g1co4BccK*E-b1D--U67vՅzzdp9xהLAʉY4"vm!U4#vs-ߵET^esPzuZ+ѣG_1q(NMAJ“QRn.*!aMy/&'P8pd7D4t"iRH*/e7 ?`7 wݨbpz[%9@t'bfY IJB[֘br0\9!۷lTvpg!H{=OPFxZ1J$sbVvB, Bn!17CL.^ ^r^Q=7}W9vhى<:K)Rz!Rsgw.⧳UJJ/. uĄALKCUPoTc>x8N 67II.G"2IIIPݻQJ~>!f,mB%hjB{29F}xLPHp_ZĮSݪ+"K|t'38rf c < D`RSQ\f&jH(?K}@$$BB'y(%%PȐ\^46.N|f ( iW%$4)qq`Rq!܄ y110|>ʂldlL!DĬ߅}{@w7\H5)1b2+G^0g^PDKBJ. &#(6k|y<=^ 8g`q nAWLXA3&SA̡w"(HVdymsNLoj"NEqYYhqbƾSK_>I[IGLA#T@„&*x'>]%jR XOTWD2h^+^.oxak\?(1[uތŮKS( Ȯ@Ðѫ ng?Ev+2 1/m\4800D[H%2ގF ' Y9K-($EB !V 6 ;5dsU>b5͍9U3o{D 1aO&:hcR2%`k7޸k|CY9]uȹ2ƵzuDq792)Ku س #ךĘGSWSA'ڈz(N̡gpr 5إB)g:e]Sw'9W䃘eݳV2`ƃ&wV64H!Qڳ$\=#-޿'矧 3gCo)#"^+l n lpoGki ρ^E)$A zj{DC@=3/7H@I[ɹ>x {SBJƂg"sfK|K^ssȒ=}S[kU{(yt5!-ҒC-Lb[ŮCxխl%]rZs#5* ?䷘mMK[ >5 qZɠu>\4R},)sȒٓkFC:E]R'zMr,&ƽJSj؝l]!e w_{kjOgE^kL?ZhFHMw\Ư q3p(ҩfIKy̙Og$ӧNļ&< #%/rnM 3 2N%!*ݰu&-ɋGzT2$ElB((Ufr5TÚ.|2˴kIrmCܶNA2=}Ộu+~B꼵!/r1a"9HDSăl\n 9LO7Xr' d8{cjK?E~GSI%A!iHIWBY<i&?\=EU 6_Ϭ Ud\'I##xAc IL$_n7x\1LM=Qūnh%%hQRM5"]]':5J+ȣ[Rc=$-ԛ(!^6` A{\ bpŔQSƊI$C}{Qd Ԃa{u 0b\D&(#N; 9{]!{Mhm3Ay* ,^֜b>˸l9`*؏otM ؘK,GkXLNk܈rrl>iWT3dUѱ W R+a7n+Kz^Y״"c)ʩ5-Id\ d!@K4SvH~pz@dwt*Tb'cj2}JR j]6I$r~GSt'蹖vx78'eKi}=}kC{.1_nMWr1>D<Vf#Yo 8hSv޹e:{!{Xh IsZM1 /!XWw3Q1xw5eZ~ I"7 d3VޑX kk)^~\9GGW1Վ8bULy>z=z}gNB>vg' O@9E TSN, bB]֣yD*!`WVVefɹ6ԎDohyNpb}#>BNx($['"k;VNL RMcbYBGlXwx#!nvKLH5)xS.VZY'($$ B(WJ00 f,x_I21E"@osD F^F[L<Ԙ!F30JL +T)_b'h,֒2eݳR!1$2} n"8kHl f|̰o*LOOo TSWd`fS=}}?xsBB 9:륷ICN2 * TǽE6 G=7}*9>"yI D9 S~5xInq14ttYg(VI$ H b6Bb\Zśn6C*&DGQx): 9 \VuJU$ݮ..sݗ}_s?>[ʰY? ygƀ DL?IgD|T4tut1ۑ`i^k(;(*!)MA%:v=*JK(@ĆdRz`5Nk&16 ܔVQJ(Ax\8{n,]5!s /`,xbјxSs)uCgei#HĬimEd76B*q=0(H^]o9!R* &5Ѡ~'I3dI~5vA/`ZsqA3[StAjSՖ}0<,Yޞ#:XH1ڏ55[܂IdPwG EN)*!ɤԐWZC=MȽ^#J ^W#lܽllٸ`}_ X@Oʠ]%nƂQ%mQxzAs=Q=0j.fjAhh@ǘJ5N*kɑ\ڙ#sb`.|/G^ه!5 B9 vjJ)ë=뵎(HuQ~cp^:UoNQOvtJ5EN$&8lsjl RHC|TV_OHk.xV63 ~M)&v]rR.y!4~b!^4BRT Ѱ?K&隸Uʅqϗ~kí3ULŜR<t9ƍʉ$uL9{{hJ3 fC(v8iACxҗ9r'BKLuqm-!%X[RAڕm,kTmAN ;sjrNFQQoaP䘔Yur-(׈vQ I\9NT\C$Ismi*d?˄xnx(@[w= ySŒ)teO1<+>ub bp!JQ%7H%f(YM*lF *b]،آ^R!Yr!󈵒pvK(ؑHdjNZ$3r˭W>Љz{dz{{q/d>z#ׅʈBQ]HZa9,f~U*9-I~mmKf8Y=ґ'}]l"hϧ,ݫqH+Jj +mcA |׸6ؾ] SM.a$b˷v לszc)=YH0CN;m!ԳʩʫDWA/ jV׀Kn7CQ}YpK[d׷!feM%K*&g2fwGx`9qdbbu9*Wzα9l7{-pGn3)-eܑcefVɢBl$SF7]JɅԒ %XXMNnKw-b#=={{z@H&f70r!If=;u!dIUnUկ2KJb 2U@7$,2(B \D zFLu*׆Z'޻#?i/_?fΒq]J/vGw/!)bo6rԄ!uV[ڜ9bqpDrq4x0LLL@b%5$]}_ EI# iS)&+LɺR$p-!RPɌGmm܊mmBL.db"w9C;JQ U FW5W'JAj]![eE9(.R mըَ%nF`n6x[@@*f$ol%5c. B^&]Ebv!z2x!S!V.WVHEMNZ$?Dgt6db*MIEEI])}mpn DR'J%f|\DM!Z.Ud#25t1(pW:1|C-468s+_b&&#QRc4!B)ꯄ:S$ cKL(W3yBH?!3O)ё݈H(3Uז N 2rWT6Crmm%w211M P:QR6CO/ #mBBD[a3]}>,T{W@M eR))n5DCl( z 5#VVqBB$)&p3o=i[e%rrfnƢ40Rى2[PRS3ЂҚ[PfKu>:L% IȎB5݈۾BEPEU'!Ϯ>kUkcH>Xo,:v~Q"[IIC droBŵȅoT{B$h縳hHP9-.vrfe=\XYC$*tohB+J+t-l*[rE5+SlOA8ҐRHa;mW-U4Իw[xWd(fȰCٽ oLDz"Bnex${Ŷ[D,bDj U";ed]`cGAAy}GjY)/pm,O*jWUF"[WzIP3Nj lOEI =Hcݪ0{CGw-%Zق\2+cT6b~/+WmC"R!beVl(@x *Z2*2,b%9"䔕şrssT(QՍ%A6(-_ :Au'G}mAzqTbbmi(% q%o+4!ǘJ9~!8sm4Ჺ ?Ewy.1DMUS!( 1|E i>%(=DBNSI[m+r-"4Ф^b"{YiCpXjVYXJ"b%5R(Șa>1g0cJ{KET#e s^? /<'@$DoǴII.vk$|6])]A6d>҆EEzTώ^ Sccb[) HS k{qPIΙ9 gf՜ VYcRu,6UҨ!D-AMʨfG jjEqb J٢6rH6~8!6ThXBkA1rHa!.eS"6*i Ou͇/C)7T QhQ Yp-b;9nM-IT\*2!"Jဋ8d3B}ȉXsA$ 9,fHN,RDR`Ԫ#ZT~ת66Y?Ӑԋ) Ł>h3&PU+VuɆjA\ c7G'!;}n[\ gnl1OچČ3ALWH2h aU۬*o>؆Yl(cr)vB9}&f CEIv@C2#f,gˍqߴmBZ 9]Kul(A.ngC;B0>y!-ܬj/WN)|x D4MI/g/K6 ~fQ^D}NDфLmݬVu^fHʉEP"\\X7Е,Q݈LSg lK^KVrò 7vr[N ^}S]R.5ȵ iYWyG6nF̨xČMf=}KZN/ƈ&'r,3E*\l=6-咥mR)1K.G dbLJBL~2ҳh7x\" :;< yЊ׽E8B+ďf g$]t[ɢE߻HwzMXZB*#Ӊ}Z Rws]N"\9YFBd2 ^FxӼe& =)`qNzMw,QlkNԐRmN r[1:H1/?KY2ՓS3oEB`_07őL/gcb[ɶtx~}N1%.kFID7 Fs-3r霋ԗnXx˦DTH%_f^0/ٶhzjth2Sd.fVL]Iv[+GΈF4)ID$K;rr^z'h+!&b` %'E4"QŶDҀd3O#g\/ɛҭx2{[frnD+= kjFrU,:M% !q,!!S/Xe̵y'nhJx'ă>< hJyL(GZoF  /V53>+6k(u< 4pVvX 5(| FK&^ux %zL7OtNh n-9Ly5q ٿ6m0= JSr߶e B a[~`0-OK_HqBN$dR 5bJgn~S:OK0(Ad[A!@[_*-cJcJ-~)$_-ə%eTaR[өs~uJP(%գLš5}9nd>PDp9LGm"29lƂq'G6˱q-b#S,U6m`1!ᎹPWw_`>pL{+]@4r_WIp)$Id\'lk||t\6+P(~r/%842/[ $:/?eU`R!k $ӽ"/PH*}噲J'[)͍)tfuXYL-w;(O+IxJVý$10y676ؠ|uO\c$*Ŏ8#]l ^6KGC2"w?p(W',M)vBi 0W謍=x#K6m/}&)YCI[il[-\*ʹheYwku7-]rL ~5? tC66-s8*%hDL&GpGtTS$zR: ,ّnm lHe7I(ZLyEhg-B5Xo1x.SM/! hOGe&ZDf9|&?%Lr <&uOE@SW$N%*{|Й,,:b2y=W2iAI7X&T |0-S#]>~ (VTAeb闠wD(P@bÀᑒrۈl 8]`I(HN]B˹4gRR64a`RhXIZY43yG[k OyI=o^9%P pBZ20O|?H zA lńtdo%::DL7 & ~!ODZ"-sU)_VG29}5Be,2ixP1"a)Q*N=a*X^ ȼA %m?rIENDB`appstream-generator-0.10.1/data/templates/default/static/img/favicon.png000066400000000000000000000101311506754475600263210ustar00rootroot00000000000000PNG  IHDR00WbKGD pHYs tIME 9}IDATh͚yUu?;!/sBhC(# ,"˶J:DYjKavYD*ZDF1U)$( C< wtRe[wsg߽Ne_>L7d+Z_f8}޵T럪,w<-~ JVu gbY(TJcT iAgr /sJW5.V8@UU^I*~zsW|RFDDUwc#Dշ; iJբE$Isc 0P(P&ʀؑx`$Nﶰ.aY't3c7v}N6mϘ/ AZGd5Ʃ2^208&u,9 "G. ,ctwujٚպ"-S Y^}w-[i&">O^g׾}l"̈"RBbDu?y\|RD14[RD>Pmuhb"2+i^AN}Fyznno:@,U^s4FU0p",@}rrOP |VM1^t݆&Sc{l*gYf /pjRpQ$= ēccIwͲyȱ 0 wՂz>g2M LgR9)l--J-'ƴ?ۊj6J6DqYdnڵZZztM4e`p,䛝eCCnhDѽs5ᑑ CCygx N? 'Hwfv "5Zz3ñW1atm;}kr ƘyS&uF'&@Ģ 7ݴxiG=c>gSbVtRuz˃O:_ c~X=~=wwxڙ9H-xu:/NLZ<^1c?=,XI\ߊ%QHL#cce۶EY}b霹7[_d~ "ABP* TIrJtm1m߾$˲llΝ$IKW,EDNJ]+'-/1ƄkX8y UU]h5SoEFc(oj:W(`DQĊ)- K{{,1oW7Q[Y#Tc9 T92k2ɡ=z(řE9p$MyѲeÐ(QyҰs|k [m\mspnCH3$!91*=o~ |.4:3@G)`8>Z!:Dex1a;En:c&O/k:޶۳cazqf;v(6= S@U}MaA`lVWi8I84x*0dڅNsh&&pcULwCHTe yޞy DV*cNLPK2~=\Fj- XUrg-W~+.U^ t51QT޳{uۆ^#A+ejC'<٧Pݺ# JbǀtZe:v71l/Mso]k3rfj;_D>%Q8Ř|ƪo^ U~)q?{{ǏikoitQWLoWc*˺4ǡj%<_?rM5.W3q\OPM 5ƛCni(Zks)1m` t+z] $i\P-LRR=zzǬ+D9͈sl^rqz˛߽_ v^N9%s&d& msR [EjLDl,ǭֽΘwa ըHF݁ϢgqFt[n`Ĭ!|}YV f766[`m;1ڜgSmwQ$D#Y(/L}9 ᛌKK}֢hS([fNDwS<Ү#MV?29*t,6yLxGƥ4}f58MOw}w}4!{1gzǜU ͲQSApQL3pDXɌnM6-!3n2pΎ:O6X%Hj ߵ CYuQ3Z7kߡbzŘ25shc-\ksߟdAr> Y]6fݪZuIvGƍltx`sSu6Yc>{mg˥&,HE20@ y\B$(`rH|`ƑK;fK'6^JXZe"Ԍ ɝмypY~pvfT[h+%鯲$zjolΚ~ПSvaX|[fc+]9L)ISmx&?Zw7ݰnol"\yC9Hƙ.M ;4l﵎ iNGyRFe# Ή5LGg . c9+/W/nm+FJ 䑩)Ui_p?ciduUӕulש)SNN>Klt#ⷹLqVSlRۙIENDB`appstream-generator-0.10.1/data/templates/default/static/img/no-image.png000066400000000000000000000017521506754475600264010ustar00rootroot00000000000000PNG  IHDR@@iqbKGD pHYs P%tIME6jǠOOϠOMJ(rx)E,-ڔ+=IxI! ^2)!Ps `9s"@"wY:JO40|(DKBFǦ^3I=UmBl|F*E^3s/o?FW: ߤ׈pE.f ]_p~U!k7U/c(#'&f5 ɱo"ޖ0 P3A20wE 0pBlS+"P G`9pM/٬60.P |EInY pV3!7ׇͰm³;tMC|@X7%9]`Җ׏'l$Qn`2M+ǣU 4sLTV(cnCi%+" Jma? |S>$WI2KOXk8(}" "$E EȩPǿ OYB-3W '@Jc1NğYCjUi,hjO]I$^A~C<Aq4ha_#G|< QOU)#r {X:a-|m+!qU8i;[-x-xl-U)b)hfY͢_)"!dƒ}IENDB`appstream-generator-0.10.1/data/templates/ubuntu000077700000000000000000000000001506754475600231102debianustar00rootroot00000000000000appstream-generator-0.10.1/docs/000077500000000000000000000000001506754475600165245ustar00rootroot00000000000000appstream-generator-0.10.1/docs/appstream-generator.1.xml000066400000000000000000000170371506754475600233750ustar00rootroot00000000000000 18 April,2016"> GNU"> GPL"> ]> appstream-generator 2016-2022 Matthias Klumpp AppStream Generator &date; appstream-generator 1 &package; Generate AppStream metadata from distribution repositories &package; Description This manual page documents briefly the &package; command. &package; generates AppStream metadata from the repositories of a software distribution. It currently supports the following repository formats / distributions: Debian, Ubuntu, Arch Linux, RPM-MD (Fedora, Mageia). The generator will produce AppStream catalog metadata files in the AppStream YAML or XML format to be shipped to users, as well as detailed HTML reports about found components and HTML and JSON reports on issues found while compiling the metadata. It reads .desktop files as well as metainfo files, renders fonts, scales images, caches screenshots etc. to produce high-quality metadata for AppStream based software centers and other tools to consume. Usually, &package; is integrated with the existing software build & delivery workflow of a distribution. The &package; tool is based on the libappstream library for metadata conversion and analysis. If you just want to embed AppStream metadata processing into another tool, using libappstream directly is likely a better choice. The generator tool does some heavy lifting like rendering fonts and scaling images, which might not be necessary for simple cases. To use &package;, a asgen-config.json file is required. Its format is described in detail in the asgen-config.json documentation. The generator supports a wide range of features that can individually configured to fit the needs of different projects and adjust the generated metadata to specific use cases. Refer to the configuration file documentation for information on what options are available. For more information about the AppStream project and the other components which are part of it, take a look at the AppStream pages at Freedesktop.org. Options Process new metadata for the given distribution suite and publish it. Cleanup old/expired metadata and media files from the cache and directories. It is recommended to run this command every week, or at least every month, depending on how many changes happen in the software repository. Export all metadata and publish reports in the export directories. You usually do not want to run this task explicitly, because it is already automatically performed by the command. Drop all valid processed metadata and hints from the selected suite. Drop all information we have about this (partial) package-id. A package-id consists of a name/version/arch triplet. For this command, the version and architecture can be omitted to forget all packages that match a particular name or name-version combination. Show information associated with this (full) package-id. A package-id consists of a name/version/arch triplet. Define the workspace location. If this flag is omitted, and no workspace directory is given in the generator configuration file, the current directory is assumed as the workspace location. This parameter, if given, overrides any workspace location defined elsewhere. Define a configuration file. Explicitly set a generator configuration JSON file. If this flag is omitted, the asgen-config.json file in the current workspace directory is used. If no workspace directory is defined in the configuration file itself, the directory it is located in is used as workspace. This can be overridden by defining a workspace explicitly with . Enforce the command. Show extra debugging information. Display the version number of &package;. See Also appstreamcli (1). AUTHOR This manual page was written by Matthias Klumpp matthias@tenstral.net. appstream-generator-0.10.1/docs/asgen-config.md000066400000000000000000000271031506754475600214110ustar00rootroot00000000000000# Generator Project Configuration This document describes the options and fields which can be set in an `asgen-config.json` file. ## JSON file example An example `asgen-config.json` file may look like this: ```json { "ProjectName": "Tanglu", "ArchiveRoot": "/srv/archive.tanglu.org/tanglu/", "MediaBaseUrl": "http://metadata.tanglu.org/appstream/media", "HtmlBaseUrl": "http://metadata.tanglu.org/appstream/", "Backend": "debian", "Features": { "processDesktop": true }, "Suites": { "chromodoris": { "sections": ["main", "contrib"], "architectures": ["amd64", "i386"] }, "chromodoris-updates": { "dataPriority": 10, "baseSuite": "chromodoris", "sections": ["main", "contrib"], "architectures": ["amd64", "i386"] } }, "Icons": { "64x64": {"cached": true, "remote": false}, "128x128": {"cached": false, "remote": true} } } ``` Note that this example assume that your packages are located in directories ``` /srv/archive.tanglu.org/tanglu/ └── chromodoris ├── contrib │   ├── amd64 │   └── i386 └── main ├── amd64 └── i386 ``` ## Description of fields ### Toplevel fields Key | Comment ------------ | ------------- ProjectName | The name of your project or distribution which ships AppStream metadata. Backend | The backend that should be used to obtain the raw data. Options are: `alpinelinux`, `archlinux`, `debian`, `dummy`, `rpmmd`, `ubuntu`. Defaults to `debian` if not set. MetadataType | The type of the resulting AppStream metadata. Can be one of `YAML` or `XML`. If omitted, the backend's default value is used. ArchiveRoot | A local URL to the mirror of your archive, containing the dists/ and pool/ directories MediaBaseUrl | The http or https URL which should be used in the generated metadata to fetch media like screenshots or icons HtmlBaseUrl | The http or https URL to the web location where the HTML hints will be published. (This setting is optional, but recommended) Oldsuites | This key exists to support migration from an alternative appstream generator. Given a list of suite names, the output HTML will link to `suitename/index.html`. Suites | Suites which should be recognized by the generator. Each suite has the components and architectures which should be searched for metadata as children. See below for more information. Features | Disable or enable selected generator features. For a detailed description see below. CAInfo | Set the CA certificate bundle file to use for SSL peer verification. If this is not set, the generator will use the system default. AllowedCustomKeys | Set which keys of the tag are allowed to be propagated to the catalog metadata output. This key takes a list of custom-key strings as value. ExportDirs | Set where to export data. The dictionary requires full paths set for the "Media", "Data", "Hints" or "Html" key. In case a value is missing, the default locations are used. ExtraMetainfoDir | Path to a directory where additional injected metainfo files are located. If not set, the `extra-metainfo` directory in the project workspace is used. WorkspaceDir | Explicitly set the location of the workspace. Only makes sense if the generator is meant to be used with a lot of configuration files and the configuration is passed to it via the `-c` flag. Icons | Customize the icon policy. See below for more details. MaxScreenshotFileSize | The maximum size of downloaded screenshot image or video files in MiB. `0` means unlimited. *Default: `14`* ### Suite fields The `Suites` field contains a dictionary of the suites which should be processed as value. These suites can contain a selection of properties: Key | Comment ------------ | ------------- sections | A list of sections the suite possesses. The "sections" are also known as archive components in the Debian world. *(required)* architectures | A list of architectures which should be processed for this suite. *(required)* baseSuite | An optional base suite name which should be used in addition to the child suite to resolve icons (only the `main` section of that suite is considered). dataPriority | An integer value representing the priority the data generated for this suite should have. Metadata with a higher priority will override existing data (think of an `-updates` suite wanting to override data shipped with the base suite). If this is not set, AppStream client tools will assume the priority being `0`. useIconTheme | Set a specific icon theme name with highest priority for this suite. This is useful if you want a different default icon theme providing icons for generic icon names (by default, the default themes of KDE and GNOME are used). immutable | If set to `true`, the state of the metadata files and exported data will be frozen, and no more changes to the data for this suite will be allowed. This only works if the `immutableSuites` feature is enabled. ### Enabling and disabling features Several features of the metadata generator can be toggled to make it work in different scenarios. The following feature values are recognized, and can be enabled or disabled in the JSON document. If no explicit value is set for a feature, the generator will pick its default value, which is sane in most cases. Name | Comment ------------ | ------------- validateMetainfo | Validate the AppStream upstream metadata. The validation is slow, but will produce better feedback and issue hints if enabled. *Default: `ON`* processDesktop | Process .desktop files which do not have a metainfo file. If disabled, all data without metainfo file will be ignored. *Default: `ON`* noDownloads | Do not attempt any downloads. This will implicitly disable any handling of screenshots and possibly other features. Using this flag is discouraged. *Default: `OFF`* createScreenshotsStore | Mirror screenshots and create thumbnails of them in `media/`. This will yield the best experience with software-centers, and also allow full control over which screenshots are displayed. Disabling this will make clients pull screenshots from 3rd-party upstream servers. *Default: `ON`* optimizePNGSize | Use `optipng` to reduce the size of PNG images. Optipng needs to be installed. *Default: `ON`* metadataTimestamps | Write timestamps into generated metadata files. *Default: `ON`* immutableSuites | Allow suites to be marked as immutable. This is useful for distributions with fixed releases, but not for rolling release distributions or continuously updated repositories. *Default: `ON`* processFonts | Include font metadata and render fonts. *Default: `ON`* allowIconUpscaling | Allows upscaling of small 48x48px icons to 64x64px to make applications show up. Icons are only upscaled as a last resort. *Default: `ON`* processGStreamer | Synthesise `type=codec` metadata from available GStreamer packages. Requires support in the backend, currently only implemented for Debian. *Default: `ON`* processLocale | Try to extract the software's localization status from Gettext data. *Default: `ON`* screenshotVideos | Permit videos in screenshots and cache them if downloads are permitted. *Default: `ON`* propagateMetaInfoArtifacts | Release artifact information is filtered out by default if a package is set for the selected metadata. Set this flag to propagate artifact information unconditionally. *Default: `OFF`* ### Configuring icon policies The `Icons` field allows to customize the icon policy used for a generator run. It decides which icon sizes are extracted, and whether they are stored as cached icon, remote icons or both. The field contains a dictionary with icon sizes as keys. Valid icon sizes are `48x48`, `64x64` and `128x128` and their HiDPI variants (e.g. `64x64@2`). The values for the icon-size keys are dictionaries with two boolean keys, `cached` and `remote`, to select the storage method for the icon size. Cached means an icon tarball is generated for the icon size that can be made available locally, while remote means the icon can be downloaded on-demand by the software center and no local cache of all icons exists. Icon sizes not mentioned, or with both `cached` and `remote` set to `false` will not be extracted. The `64x64` icon size must always be present and be cached. If this is not the case, appstream-generator will adjust the configuration internally and emit a warning. If no `Icons` field is present, appstream-generator will use a default policy for icons (creating cache tarballs for all sizes, and remote links for sizes >= 129x128px). ## Injecting extra metainfo / removing components Sometimes injecting metainfo files directly into the generation process instead of packaging them makes sense. This can be done for example for `web-application` components, `operating-system` components or for components which are merged into others. It is discouraged to use this feature for any components that are directly tied to a package. Metainfo XML files can be placed in `%{ExtraMetainfoDir}/suite/section/(arch)`, where `%{ExtraMetainfoDir}` usually is the `extra-metainfo` directory in the generator's current workspace, unless this default is overriden in the configuration file. If the `arch` part of the path is omitted, a metainfo file is added to all active architectures. E.g. a XML file placed in `%{ExtraMetainfoDir}/buster/main/` will apply to all active architectures for `buster`, while files in `%{ExtraMetainfoDir}/buster/main/amd64/` will only apply to the `amd64` architecture. Additionally to metainfo XML files, a `modifications.json` file may be placed in the `extra-metainfo` directories. This JSON file can contain a dictionary with a `Remove` entry and a `InjectCustom` entry. The `Remove` entry has a list of component IDs that should be removed from the metadata pool on client machines as value. By listing IDs in this file, the generator will create a special component that will trigger a component with the same ID to be removed from the metadata pool if its priority is lower. This feature is useful for overlay suites, for example if the `buster-updates` suite (with priority 10) wants to completely remove a component from the `buster` suite (with priority 0) from the user's eyes. This may be necessary if a component improperly changes its ID, if you are maintaining an overlay distribution and can not control the composition of packages in the base suites, or in case of legal issues where a component has to be removed retroactively from a frozen distribution suite. It may also be useful to hide `web-application` components in a stable distribution in case their service is discontinued before the distribution release is out of support as well. The `InjectCustom` entry contains a dictionary of string to dictionary mappings that contain values that should be injected into a component's `` tag. It can be used to apply values specific to the perticular vendor's repository. Example `modifications.json` file: ```json { "Remove": [ "com.example.removed" ], "InjectCustom": { "org.example.mediaplayer": {"Vendor::featured": "yes", "Vendor::price": "42EUR"} } } ``` Internally, injected metainfo data is grouped under a special fake package name, in order to allow the generator to properly record hints for the added components and to associate the data with the right suite(s). ## Minimal configuration file A minimal configuration file can look like this: ```json { "ProjectName": "Tanglu", "ArchiveRoot": "/srv/archive.tanglu.org/tanglu/", "MediaBaseUrl": "http://metadata.tanglu.org/appstream/media", "HtmlBaseUrl": "http://metadata.tanglu.org/appstream/", "Backend": "debian", "Suites": { "chromodoris": { "sections": ["main", "contrib"], "architectures": ["amd64", "i386"] } } } ``` appstream-generator-0.10.1/docs/index.md000066400000000000000000000002271506754475600201560ustar00rootroot00000000000000# AppStream Generator Docs For general information check out the [README file](../README.md) See [usage.md](usage.md) for general usage information. appstream-generator-0.10.1/docs/meson.build000066400000000000000000000012051506754475600206640ustar00rootroot00000000000000# Meson definition for AppStream Generator Documentation # make manual page xsltproc = find_program('xsltproc') custom_target('man-asgen', input: 'appstream-generator.1.xml', output: 'appstream-generator.1', install: true, install_dir: join_paths(get_option('mandir'), 'man1'), command: [ xsltproc, '--nonet', '--stringparam', 'man.output.quietly', '1', '--stringparam', 'funcsynopsis.style', 'ansi', '--stringparam', 'man.th.extra1.suppress', '1', '-o', '@OUTPUT@', 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl', '@INPUT@' ] ) appstream-generator-0.10.1/docs/usage.md000066400000000000000000000045651506754475600201640ustar00rootroot00000000000000# AppStream Generator Usage ## How to use ### Generating distro metadata To generate AppStream distribution metadata for your repository, create a local mirror of the repository first. Then create a new folder, and write a `asgen-config.json` configuration file for the metadata generator. Details on the file and an example can be found in [the asgen-config docs](asgen-config.md). After the config file has been written, you can generate the metadata as follows: ```Bash cd /srv/asgen/workspace # path where the asgen-config.json file is located appstream-generator process chromodoris # replace "chromodoris" with the name of the suite you want to analyze ``` The generator is assuming you have enough memory and disk space on your machine to cache stuff. Resulting metadata will be placed in `export/data/`, machine-readable issue-hints can be found in `export/hints/` and the processed screenshots and icons are located in `export/media/`. In order to drop old packages and cruft from the databases, you should run ```Bash appstream-generator cleanup ``` every once in a while. This will drop all superseded packages and data from the caches. If you do not want to `cd` into the workspace directory, you can also use the `--workspace|-w` flag to define a workspace. ### Validating metadata You can validate the resulting metadata using the AppStream client tools. Use `appstreamcli validate .xml.gz` for XML metadata, and `dep11-validate .yml.gz` for YAML. This will check the files for mistakes and compliance with the specification. Keep in mind that the generator will always generate spec-compliant metadata, but might - depending on the input - produce data which has smaller flaws (e.g. formatting issues in the long descriptions). In these cases, issue-hints will have been emitted, so the package maintainers can address the metadata issues. ## Troubleshooting ### Memory Usage The `appstream-generator` will not hesitate to use RAM, and also decide to use lots of it if enough is available. This is especially true when scanning new packages for their contents and storing the information in the LMDB database. Ideally make sure that you are running a 64bit system with at least 4GB of RAM if you want to use the generator properly. For the generator, speed matters more than RAM usage. You can use cgroups to limit the amount of memory the generator uses. ### Profiling TODO appstream-generator-0.10.1/meson.build000066400000000000000000000104561506754475600177440ustar00rootroot00000000000000project('AppStream Generator', 'cpp', meson_version : '>=1.0', default_options : ['c_std=c23', 'cpp_std=c++23'], subproject_dir : 'contrib/subprojects', license : 'LGPL-3.0+', version : '0.10.1' ) asgen_version = meson.project_version() source_root = meson.project_source_root() build_root = meson.project_build_root() cxx = meson.get_compiler('cpp') fs = import('fs') # # Dependencies # src_dir = include_directories('src/') glib_dep = dependency('glib-2.0', version: '>= 2.80') appstream_dep = dependency('appstream', version: '>= 1.1.0') ascompose_dep = dependency('appstream-compose', version: '>= 1.1.0') lmdb_dep = dependency('lmdb', version: '>= 0.9.22') archive_dep = dependency('libarchive', version: '>= 3.2') curl_dep = dependency('libcurl') fyaml_dep = dependency('libfyaml', version: '>= 0.8') tbb_dep = dependency('tbb') icu_dep = dependency('icu-uc') inja_dep = dependency('inja', fallback: ['inja', 'inja_dep'], default_options: ['build_tests=false']) libxml2_dep = dependency('libxml-2.0') # for rpmmd catch2_dep = dependency('catch2-with-main') backward_deps = [] backward_dep = dependency('', required: false) if get_option('backward') backward_dep = dependency('backward-cpp', fallback: ['backward-cpp', 'backward_dep'], required: false) if backward_dep.found() message('Using backward-cpp for stack traces') libunwind_dep = dependency('libunwind') backward_deps = [backward_dep, libunwind_dep] endif endif # Ensure we have a compiler that can handle the C++23 features we need if cxx.get_id() == 'gcc' if not cxx.version().version_compare('>=14.0') error('GCC 14 or newer is required to build this project') endif elif cxx.get_id() == 'clang' if not cxx.version().version_compare('>=18.0') error('Clang 18 or newer is required to build this project') endif endif # # Compiler flags # add_project_arguments('-D_POSIX_C_SOURCE=201710L', language: 'c') add_project_arguments('-D_POSIX_C_SOURCE=201710L', language: 'cpp') if get_option('maintainer') maintainer_c_args = [ '-Werror', '-Wall', '-Wextra', '-Wcast-align', '-Wno-uninitialized', '-Wempty-body', '-Wformat-security', '-Winit-self', '-Wnull-dereference', '-Winline', '-Wmaybe-uninitialized', ] maintainer_cpp_args = [ '-Wsuggest-final-methods' ] add_project_arguments(maintainer_c_args, language: 'c') add_project_arguments([maintainer_c_args, maintainer_cpp_args], language: 'cpp') endif # a few compiler warning/error flags we always want enabled add_project_arguments( '-Werror=shadow', '-Werror=empty-body', '-Werror=missing-prototypes', '-Werror=implicit-function-declaration', '-Werror=missing-declarations', '-Werror=return-type', '-Werror=int-conversion', '-Werror=incompatible-pointer-types', '-Werror=misleading-indentation', '-Werror=format-security', '-Wno-missing-field-initializers', '-Wno-error=missing-field-initializers', '-Wno-unused-parameter', '-Wno-error=unused-parameter', language: 'c' ) add_project_arguments( cxx.get_supported_arguments([ '-Werror=empty-body', '-Werror=pointer-arith', '-Werror=missing-declarations', '-Werror=return-type', '-Werror=misleading-indentation', '-Werror=format-security', #'-Werror=suggest-override', -- we can't enable this, Inja is missing overrides '-Wno-missing-field-initializers', '-Wno-error=missing-field-initializers', '-Wno-unused-parameter', '-Wno-error=unused-parameter', ]), language: 'cpp' ) # # Download JS stuff and additional sources if we couldn't find them # if get_option('download-js') npm_exe = find_program('npm') if not fs.is_dir(source_root / 'data' / 'templates' / 'default' / 'static' / 'js') message('Downloading JavaScript libraries...') getjs_cmd = run_command([source_root + '/contrib/setup/build_js.sh', npm_exe], check: false) if getjs_cmd.returncode() != 0 error('Unable to download JavaScript files with NPM:\n' + getjs_cmd.stdout() + getjs_cmd.stderr()) endif endif endif # # Subdirs # subdir('src') subdir('data') subdir('tests') subdir('docs') appstream-generator-0.10.1/meson_options.txt000066400000000000000000000007371506754475600212400ustar00rootroot00000000000000# # Options for AppStream Generator # option('download-js', type: 'boolean', value: true, description: 'Download JavaScript with NPM automatically.' ) option('backward', type : 'boolean', value : true, description : 'Use stack trace pretty-printing using backward-cpp.' ) option('maintainer', type : 'boolean', value : false, description : 'Compile in maintainer mode (use strict compiler flags, e.g. -Werror)' ) appstream-generator-0.10.1/snapcraft.yaml000066400000000000000000000150341506754475600204440ustar00rootroot00000000000000# # Copyright (C) 2020 Matthias Klumpp # Copyright (C) 2020 Canonical Ltd # Author: Iain Lane # # SPDX-License-Identifier: FSFAP # # This Snapcraft file and the resulting Snap is NOT OFFICIALLY SUPPORTED. # If you can, please use a provided native build for your distribution. # Patches to improve this file are very welcome though, and so is testing the Snap! # # This snap is strict, but it has an important limitation: # - it needs read/write access to wherever the user puts the workspace # definition (could be anywhere, but most commonly it's in /srv, in /home or # in /var), access to a local archive mount (can be in /mnt), and that should # be it (unless optipng and ffprobe have extra requirements). # # Until that is overcome, this snap should be installed using `--devmode` to # effectively disable the confinement. # # This Snap is autobuilt, you can check its status at https://build.snapcraft.io/user/ximion/appstream-generator name: appstream-generator license: LGPL-3.0 base: core24 adopt-info: appstream-generator confinement: strict grade: stable platforms: amd64: arm64: apps: appstream-generator: command: usr/bin/appstream-generator extensions: [gnome] common-id: org.freedesktop.appstream.generator plugs: - network - home environment: GSETTINGS_SCHEMA_DIR: "$SNAP/usr/share/glib-2.0/schemas" GIO_EXTRA_MODULES: "$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gio/modules" layout: /usr/share/appstream: bind: $SNAP/usr/share/appstream /usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gdk-pixbuf-2.0: bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gdk-pixbuf-2.0 /usr/share/i18n: bind: $SNAP/usr/share/i18n /usr/share/mime: bind: $SNAP/usr/share/mime parts: libfyaml: source: https://github.com/pantoniou/libfyaml.git source-type: git source-tag: 'v0.9' plugin: autotools autotools-configure-parameters: - --prefix=/usr/ - --libdir=/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR - --disable-static - --enable-shared build-packages: - build-essential - pkgconf appstream: source: https://github.com/ximion/appstream.git source-type: git source-tag: 'v1.1.0' plugin: meson build-environment: - DESTDIR: $CRAFT_PART_INSTALL - LD_LIBRARY_PATH: > $CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR: $CRAFT_STAGE/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} meson-parameters: - --prefix=/usr - --buildtype=debugoptimized - -Dcompose=true - -Dapidocs=false - -Dsystemd=false after: - libfyaml build-packages: - build-essential - docbook-xml - docbook-xsl - gettext - gobject-introspection - gperf - itstool - libcurl4-gnutls-dev - liblzma-dev - libffi-dev - libgirepository1.0-dev - libglib2.0-dev - libstemmer-dev - libxml2-dev - libxmlb-dev - libzstd-dev - xsltproc stage-packages: - libcurl3t64-gnutls - libicu74 - libstemmer0d - libxml2 - libzstd1 appstream-generator: source: . source-type: git parse-info: [usr/share/metainfo/org.freedesktop.appstream.generator.metainfo.xml] override-pull: | craftctl default # set version from Git craftctl set version=$(git describe --always | sed -e 's/v//;s/-/+git/;y/-/./') build-environment: - CC: gcc-14 - CXX: g++-14 - NINJAFLAGS: -v - PATH: > $CRAFT_STAGE/usr/bin:$CRAFT_STAGE/usr/sbin:${PATH} - PKG_CONFIG_PATH: > $CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/pkgconfig: $CRAFT_STAGE/usr/lib/pkgconfig: $CRAFT_STAGE/usr/share/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH} - LD_LIBRARY_PATH: > $CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR: $CRAFT_STAGE/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} plugin: meson meson-parameters: - --prefix=/usr - --buildtype=debugoptimized - -Ddownload-js=true override-build: | set -eux craftctl default meson test -C "${CRAFT_PART_BUILD}" --print-errorlogs --no-stdsplit --verbose override-prime: | set -eux craftctl default for dir in usr/lib/*/gdk-pixbuf-2.0; do if [ -d "${dir}" ]; then for subdir in "${dir}"/*; do if [ -d "${subdir}" ] && [ -d "${subdir}/loaders" ]; then GDK_PIXBUF_MODULEDIR="$(pwd)/${subdir}/loaders" "${dir}/gdk-pixbuf-query-loaders" > "${subdir}/loaders.cache" sed -i "s,$(pwd),," "${subdir}/loaders.cache" fi done fi done update-mime-database -V usr/share/mime/ usr/lib/${CRAFT_ARCH_TRIPLET}/glib-2.0/glib-compile-schemas usr/share/glib-2.0/schemas after: - appstream - libfyaml build-packages: - g++-14 - gcc-14 - curl - docbook-xsl - docbook-xml - ffmpeg - npm - catch2 - libarchive-dev - libbackward-cpp-dev - libcairo2-dev - libcurl4-gnutls-dev - libfontconfig1-dev - libfreetype6-dev - libgdk-pixbuf2.0-dev - libglib2.0-dev - liblmdb-dev - libpango1.0-dev - librsvg2-dev - libtbb-dev - libunwind-dev - libxml2-dev - pkgconf - xsltproc stage-packages: - ffmpeg - gdb - glib-networking - libarchive13t64 - libc-bin - libcairo2 - libcurl3t64-gnutls - libdatrie1 - libfontconfig1 - libfreetype6 - libgdk-pixbuf2.0-0 - libglib2.0-0t64 - libglib2.0-bin - libglu1-mesa - libglut3.12 - libgraphite2-3 - libharfbuzz0b #- libjxl-gdk-pixbuf - liblmdb0 - libpango-1.0-0 - libpangocairo-1.0-0 - libpangoft2-1.0-0 - libpixman-1-0 - libpng16-16t64 - librsvg2-2 - librsvg2-common - libslang2 - libthai0 - libx11-6 - libxau6 - libxcb-render0 - libxcb-shm0 - libxcb1 - libxdmcp6 - libxext6 - libxrender1 - locales - locales-all - optipng - shared-mime-info prime: - -usr/lib/*/libglib-* - -usr/lib/*/libgio* - -usr/lib/*/libgmodule* - -usr/lib/*/libgobject* - -usr/lib/*/libgthread* - -usr/lib/*/pkgconfig/gio* - -usr/lib/*/pkgconfig/glib-2.0.pc - -usr/lib/*/pkgconfig/gmodule* - -usr/lib/*/pkgconfig/gobject-2.0.pc - -usr/lib/*/pkgconfig/gthread-2.0.pc appstream-generator-0.10.1/src/000077500000000000000000000000001506754475600163635ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/000077500000000000000000000000001506754475600201355ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/alpinelinux/000077500000000000000000000000001506754475600224655ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/alpinelinux/apkindexutils.cpp000066400000000000000000000120761506754475600260630ustar00rootroot00000000000000/* * Copyright (C) 2020-2025 Rasmus Thomsen * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "apkindexutils.h" #include #include #include #include "../../utils.h" #include "../../logging.h" namespace fs = std::filesystem; namespace ASGenerator { ApkIndexBlockRange::ApkIndexBlockRange(const std::string &contents) : m_lineDelta(0), m_empty(false) { m_lines = Utils::splitString(contents, '\n'); getNextBlock(); } void ApkIndexBlockRange::getNextBlock() { std::vector completePair; std::size_t iterations = 0; m_currentBlock = ApkIndexBlock{}; for (std::size_t i = m_lineDelta; i < m_lines.size(); ++i) { const auto ¤tLine = m_lines[i]; iterations++; if (currentLine.empty()) { // next block for next package started break; } if (currentLine.find(':') != std::string::npos) { if (completePair.empty()) { completePair = {currentLine}; continue; } const auto joinedPair = Utils::joinStrings(completePair, " "); const auto colonPos = joinedPair.find(':'); if (colonPos != std::string::npos) { const auto key = joinedPair.substr(0, colonPos); const auto value = joinedPair.substr(colonPos + 1); setCurrentBlock(key, value); } completePair = {currentLine}; } else { completePair.push_back(Utils::trimString(currentLine)); } } // Handle the last pair if we reached the end if (!completePair.empty()) { const auto joinedPair = Utils::joinStrings(completePair, " "); const auto colonPos = joinedPair.find(':'); if (colonPos != std::string::npos) { const auto key = joinedPair.substr(0, colonPos); const auto value = joinedPair.substr(colonPos + 1); setCurrentBlock(key, value); } } m_lineDelta += iterations; m_empty = (m_lineDelta >= m_lines.size()); } void ApkIndexBlockRange::setCurrentBlock(const std::string &key, const std::string &value) { const auto trimmedValue = Utils::trimString(value); if (key == "P") { m_currentBlock.pkgname = trimmedValue; } else if (key == "V") { m_currentBlock.pkgversion = trimmedValue; } else if (key == "A") { m_currentBlock.arch = trimmedValue; } else if (key == "m") { m_currentBlock.maintainer = trimmedValue; } else if (key == "T") { m_currentBlock.pkgdesc = trimmedValue; } // Ignore other fields for now } const ApkIndexBlock &ApkIndexBlockRange::front() const { return m_currentBlock; } bool ApkIndexBlockRange::empty() const { return m_empty; } void ApkIndexBlockRange::popFront() { getNextBlock(); } // Iterator implementation ApkIndexBlockRange::iterator::iterator(ApkIndexBlockRange *range, bool isEnd) : m_range(range), m_isEnd(isEnd) { } const ApkIndexBlock &ApkIndexBlockRange::iterator::operator*() const { return m_range->front(); } const ApkIndexBlock *ApkIndexBlockRange::iterator::operator->() const { return &m_range->front(); } ApkIndexBlockRange::iterator &ApkIndexBlockRange::iterator::operator++() { m_range->popFront(); if (m_range->empty()) { m_isEnd = true; } return *this; } bool ApkIndexBlockRange::iterator::operator!=(const iterator &other) const { return m_isEnd != other.m_isEnd; } ApkIndexBlockRange::iterator ApkIndexBlockRange::begin() { return iterator(this, empty()); } ApkIndexBlockRange::iterator ApkIndexBlockRange::end() { return iterator(this, true); } std::string downloadIfNecessary( const std::string &apkRootPath, const std::string &tmpDir, const std::string &fileName, const std::string &cacheFileName) { const std::string fullPath = (fs::path(apkRootPath) / fileName).string(); const std::string cachePath = (fs::path(tmpDir) / cacheFileName).string(); if (Utils::isRemote(fullPath)) { fs::create_directories(tmpDir); auto &dl = Downloader::get(); dl.downloadFile(fullPath, cachePath); return cachePath; } else { if (fs::exists(fullPath)) return fullPath; else throw std::runtime_error(std::format("File '{}' does not exist.", fullPath)); } } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/alpinelinux/apkindexutils.h000066400000000000000000000050061506754475600255230ustar00rootroot00000000000000/* * Copyright (C) 2020-2025 Rasmus Thomsen * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include "../../downloader.h" namespace ASGenerator { /** * Struct representing a block inside of an APKINDEX. Each block, separated by * a newline, contains information about exactly one package. */ struct ApkIndexBlock { std::string arch; std::string maintainer; std::string pkgname; std::string pkgversion; std::string pkgdesc; std::string archiveName() const { return std::format("{}-{}.apk", pkgname, pkgversion); } }; /** * Range for looping over the contents of an APKINDEX, block by block. */ class ApkIndexBlockRange { public: explicit ApkIndexBlockRange(const std::string &contents); const ApkIndexBlock &front() const; bool empty() const; void popFront(); // Iterator interface for range-based for loops class iterator { private: ApkIndexBlockRange *m_range; bool m_isEnd; public: explicit iterator(ApkIndexBlockRange *range, bool isEnd = false); const ApkIndexBlock &operator*() const; const ApkIndexBlock *operator->() const; iterator &operator++(); bool operator!=(const iterator &other) const; }; iterator begin(); iterator end(); private: std::vector m_lines; std::size_t m_lineDelta; ApkIndexBlock m_currentBlock; bool m_empty; void getNextBlock(); void setCurrentBlock(const std::string &key, const std::string &value); }; /** * Download APK index file if necessary */ std::string downloadIfNecessary( const std::string &apkRootPath, const std::string &tmpDir, const std::string &fileName, const std::string &cacheFileName); } // namespace ASGenerator appstream-generator-0.10.1/src/backends/alpinelinux/apkpkg.cpp000066400000000000000000000067321506754475600244560ustar00rootroot00000000000000/* * Copyright (C) 2020-2025 Rasmus Thomsen * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "apkpkg.h" #include #include #include "../../config.h" #include "../../downloader.h" #include "../../utils.h" #include "../../zarchive.h" namespace fs = std::filesystem; namespace ASGenerator { AlpinePackage::AlpinePackage(const std::string &pkgname, const std::string &pkgver, const std::string &pkgarch) : m_pkgname(pkgname), m_pkgver(pkgver), m_pkgarch(pkgarch), m_archive(std::make_unique()) { const auto &conf = Config::get(); m_tmpDir = conf.getTmpDir() / std::format("{}-{}_{}", name(), ver(), arch()); } std::string AlpinePackage::name() const { return m_pkgname; } void AlpinePackage::setName(const std::string &val) { m_pkgname = val; } std::string AlpinePackage::ver() const { return m_pkgver; } void AlpinePackage::setVersion(const std::string &val) { m_pkgver = val; } std::string AlpinePackage::arch() const { return m_pkgarch; } void AlpinePackage::setArch(const std::string &val) { m_pkgarch = val; } const std::unordered_map &AlpinePackage::description() const { return m_desc; } void AlpinePackage::setFilename(const std::string &fname) { m_pkgFname = fname; } std::string AlpinePackage::getFilename() { if (!m_localPkgFName.empty()) return m_localPkgFName; if (Utils::isRemote(m_pkgFname)) { std::lock_guard lock(m_mutex); auto &dl = Downloader::get(); const auto path = m_tmpDir / fs::path(m_pkgFname).filename(); dl.downloadFile(m_pkgFname, path.string()); m_localPkgFName = path.string(); return m_localPkgFName; } else { m_localPkgFName = m_pkgFname; return m_localPkgFName; } } std::string AlpinePackage::maintainer() const { return m_pkgmaintainer; } void AlpinePackage::setMaintainer(const std::string &maint) { m_pkgmaintainer = maint; } void AlpinePackage::setDescription(const std::string &text, const std::string &locale) { m_desc[locale] = text; } std::vector AlpinePackage::getFileData(const std::string &fname) { if (!m_archive->isOpen()) m_archive->open(getFilename()); return m_archive->readData(fname); } const std::vector &AlpinePackage::contents() { if (!m_contentsL.empty()) return m_contentsL; ArchiveDecompressor ad; ad.open(getFilename()); m_contentsL = ad.readContents(); return m_contentsL; } void AlpinePackage::setContents(const std::vector &c) { m_contentsL = c; } void AlpinePackage::finish() { // No-op for Alpine package } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/alpinelinux/apkpkg.h000066400000000000000000000047341506754475600241230ustar00rootroot00000000000000/* * Copyright (C) 2020-2025 Rasmus Thomsen * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include "../interfaces.h" #include "../../utils.h" namespace ASGenerator { class ArchiveDecompressor; class AlpinePackage : public Package { public: AlpinePackage(const std::string &pkgname, const std::string &pkgver, const std::string &pkgarch); ~AlpinePackage() override = default; std::string name() const override; void setName(const std::string &val); std::string ver() const override; void setVersion(const std::string &val); std::string arch() const override; void setArch(const std::string &val); const std::unordered_map &description() const override; void setFilename(const std::string &fname); std::string getFilename() override; std::string maintainer() const override; void setMaintainer(const std::string &maint); void setDescription(const std::string &text, const std::string &locale); std::vector getFileData(const std::string &fname) override; const std::vector &contents() override; void setContents(const std::vector &c); void finish() override; private: std::string m_pkgname; std::string m_pkgver; std::string m_pkgarch; std::string m_pkgmaintainer; std::unordered_map m_desc; std::string m_pkgFname; fs::path m_localPkgFName; fs::path m_tmpDir; std::vector m_contentsL; std::unique_ptr m_archive; mutable std::mutex m_mutex; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/alpinelinux/apkpkgindex.cpp000066400000000000000000000155141506754475600255040ustar00rootroot00000000000000/* * Copyright (C) 2020-2025 Rasmus Thomsen * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "apkpkgindex.h" #include #include #include #include "../../config.h" #include "../../logging.h" #include "../../zarchive.h" #include "../../utils.h" #include "../../downloader.h" #include "apkindexutils.h" namespace fs = std::filesystem; namespace ASGenerator { AlpinePackageIndex::AlpinePackageIndex(const std::string &dir) : m_rootDir(dir) { if (!Utils::isRemote(dir) && !fs::exists(dir)) throw std::runtime_error(std::format("Directory '{}' does not exist.", dir)); const auto &conf = Config::get(); m_tmpDir = conf.getTmpDir() / fs::path(dir).filename(); } void AlpinePackageIndex::release() { m_pkgCache.clear(); } void AlpinePackageIndex::setPkgDescription(std::shared_ptr pkg, const std::string &pkgDesc) { if (pkgDesc.empty()) return; const std::string desc = std::format("

{}

", Utils::escapeXml(pkgDesc)); pkg->setDescription(desc, "C"); } std::string AlpinePackageIndex::downloadIfNecessary( const std::string &apkRootPath, const std::string &tmpDir, const std::string &fileName, const std::string &cacheFileName) { const std::string fullPath = (fs::path(apkRootPath) / fileName).string(); const std::string cachePath = (fs::path(tmpDir) / cacheFileName).string(); if (Utils::isRemote(fullPath)) { fs::create_directories(tmpDir); auto &dl = Downloader::get(); dl.downloadFile(fullPath, cachePath); return cachePath; } else { if (fs::exists(fullPath)) return fullPath; else throw std::runtime_error(std::format("File '{}' does not exist.", fullPath)); } } std::vector AlpinePackageIndex::parseApkIndex(const std::string &indexString) { std::vector entries; const auto lines = Utils::splitString(indexString, '\n'); ApkIndexEntry currentEntry; for (const auto &line : lines) { if (line.empty()) { // End of package entry if (!currentEntry.pkgname.empty()) { entries.push_back(std::move(currentEntry)); currentEntry = ApkIndexEntry{}; } continue; } if (line.length() < 2 || line[1] != ':') continue; const char field = line[0]; const std::string value = line.substr(2); switch (field) { case 'P': // Package name currentEntry.pkgname = value; break; case 'V': // Version currentEntry.pkgversion = value; break; case 'A': // Architecture currentEntry.arch = value; break; case 'F': // Filename currentEntry.archiveName = value; break; case 'm': // Maintainer currentEntry.maintainer = value; break; case 'T': // Description currentEntry.pkgdesc = value; break; default: // Ignore other fields break; } } // Add the last entry if not empty if (!currentEntry.pkgname.empty()) entries.push_back(std::move(currentEntry)); return entries; } std::vector> AlpinePackageIndex::loadPackages( const std::string &suite, const std::string §ion, const std::string &arch) { const auto apkRootPath = m_rootDir / suite / section / arch; const auto cacheFileName = std::format("APKINDEX-{}-{}-{}.tar.gz", suite, section, arch); const auto indexFPath = ASGenerator::downloadIfNecessary( apkRootPath.string(), m_tmpDir, "APKINDEX.tar.gz", cacheFileName); std::unordered_map> pkgsMap; ArchiveDecompressor ad; ad.open(indexFPath); const auto indexData = ad.readData("APKINDEX"); const std::string indexString(indexData.begin(), indexData.end()); // Use the proper ApkIndexBlockRange from the utilities ApkIndexBlockRange range(indexString); for (const auto &pkgInfo : range) { const auto &fileName = pkgInfo.archiveName(); std::shared_ptr pkg; auto it = pkgsMap.find(fileName); if (it != pkgsMap.end()) { pkg = it->second; } else { pkg = std::make_shared(pkgInfo.pkgname, pkgInfo.pkgversion, pkgInfo.arch); pkgsMap[fileName] = pkg; } pkg->setFilename((fs::path(m_rootDir) / suite / section / arch / fileName).string()); pkg->setMaintainer(pkgInfo.maintainer); setPkgDescription(std::move(pkg), pkgInfo.pkgdesc); } // Perform a sanity check, so we will never emit invalid packages std::vector> packages; packages.reserve(pkgsMap.size()); for (const auto &[fileName, pkg] : pkgsMap) { if (!pkg->isValid()) { logWarning("Found invalid package ({})! Skipping it.", pkg->toString()); continue; } packages.push_back(std::static_pointer_cast(pkg)); } return packages; } std::vector> AlpinePackageIndex::packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs) { const std::string id = std::format("{}/{}/{}", suite, section, arch); auto it = m_pkgCache.find(id); if (it == m_pkgCache.end()) { auto pkgs = loadPackages(suite, section, arch); m_pkgCache[id] = pkgs; return pkgs; } return it->second; } std::shared_ptr AlpinePackageIndex::packageForFile( const std::string &fname, const std::string &suite, const std::string §ion) { // Not implemented for Alpine Linux backend return nullptr; } bool AlpinePackageIndex::hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) { // For simplicity, always assume changes for Alpine Linux // In a real implementation, you'd check modification times of APKINDEX files return true; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/alpinelinux/apkpkgindex.h000066400000000000000000000050231506754475600251430ustar00rootroot00000000000000/* * Copyright (C) 2020-2025 Rasmus Thomsen * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include "../interfaces.h" #include "../../utils.h" #include "apkpkg.h" namespace ASGenerator { struct ApkIndexEntry { std::string pkgname; std::string pkgversion; std::string arch; std::string archiveName; std::string maintainer; std::string pkgdesc; }; class AlpinePackageIndex : public PackageIndex { public: explicit AlpinePackageIndex(const std::string &dir); void release() override; std::vector> packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true) override; std::shared_ptr packageForFile( const std::string &fname, const std::string &suite = "", const std::string §ion = "") override; bool hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) override; private: fs::path m_rootDir; fs::path m_tmpDir; std::unordered_map>> m_pkgCache; void setPkgDescription(std::shared_ptr pkg, const std::string &pkgDesc); std::vector> loadPackages( const std::string &suite, const std::string §ion, const std::string &arch); std::vector parseApkIndex(const std::string &indexString); std::string downloadIfNecessary( const std::string &apkRootPath, const std::string &tmpDir, const std::string &fileName, const std::string &cacheFileName); }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/alpinelinux/meson.build000066400000000000000000000001771506754475600246340ustar00rootroot00000000000000 backend_alpine_src = files( 'apkindexutils.cpp', 'apkpkg.cpp', 'apkpkgindex.cpp', ) backends_src += backend_alpine_src appstream-generator-0.10.1/src/backends/archlinux/000077500000000000000000000000001506754475600221325ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/archlinux/alpkg.cpp000066400000000000000000000046071506754475600237430ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "alpkg.h" #include "../../logging.h" #include "../../zarchive.h" namespace ASGenerator { ArchPackage::ArchPackage() : m_archive(std::make_unique()) { } std::string ArchPackage::name() const { return m_pkgname; } void ArchPackage::setName(const std::string &val) { m_pkgname = val; } std::string ArchPackage::ver() const { return m_pkgver; } void ArchPackage::setVersion(const std::string &val) { m_pkgver = val; } std::string ArchPackage::arch() const { return m_pkgarch; } void ArchPackage::setArch(const std::string &val) { m_pkgarch = val; } const std::unordered_map &ArchPackage::description() const { return m_desc; } void ArchPackage::setFilename(const std::string &fname) { m_pkgFname = fname; } std::string ArchPackage::getFilename() { return m_pkgFname; } std::string ArchPackage::maintainer() const { return m_pkgmaintainer; } void ArchPackage::setMaintainer(const std::string &maint) { m_pkgmaintainer = maint; } void ArchPackage::setDescription(const std::string &text, const std::string &locale) { m_desc[locale] = text; } std::vector ArchPackage::getFileData(const std::string &fname) { if (!m_archive->isOpen()) m_archive->open(getFilename()); return m_archive->readData(fname); } const std::vector &ArchPackage::contents() { return m_contentsL; } void ArchPackage::setContents(const std::vector &c) { m_contentsL = c; } void ArchPackage::finish() { // No-op for Arch Linux package } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/archlinux/alpkg.h000066400000000000000000000043341506754475600234050ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include "../interfaces.h" namespace ASGenerator { class ArchiveDecompressor; class ArchPackage : public Package { public: ArchPackage(); ~ArchPackage() override = default; std::string name() const override; void setName(const std::string &val); std::string ver() const override; void setVersion(const std::string &val); std::string arch() const override; void setArch(const std::string &val); const std::unordered_map &description() const override; void setFilename(const std::string &fname); std::string getFilename() override; std::string maintainer() const override; void setMaintainer(const std::string &maint); void setDescription(const std::string &text, const std::string &locale); std::vector getFileData(const std::string &fname) override; const std::vector &contents() override; void setContents(const std::vector &c); void finish() override; private: std::string m_pkgname; std::string m_pkgver; std::string m_pkgarch; std::string m_pkgmaintainer; std::unordered_map m_desc; std::string m_pkgFname; std::vector m_contentsL; std::unique_ptr m_archive; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/archlinux/alpkgindex.cpp000066400000000000000000000125251506754475600247710ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "alpkgindex.h" #include #include #include "../../logging.h" #include "../../zarchive.h" #include "../../utils.h" namespace ASGenerator { ArchPackageIndex::ArchPackageIndex(const std::string &dir) : m_rootDir(dir) { if (!fs::exists(dir)) throw std::runtime_error(std::format("Directory '{}' does not exist.", dir)); } void ArchPackageIndex::release() { m_pkgCache.clear(); } void ArchPackageIndex::setPkgDescription(std::shared_ptr pkg, const std::string &pkgDesc) { if (pkgDesc.empty()) return; const std::string desc = std::format("

{}

", Utils::escapeXml(pkgDesc)); pkg->setDescription(desc, "C"); } std::vector> ArchPackageIndex::loadPackages( const std::string &suite, const std::string §ion, const std::string &arch) { const auto pkgRoot = m_rootDir / suite / section / "os" / arch; const auto listsTarFname = pkgRoot / std::format("{}.files.tar.gz", section); if (!fs::exists(listsTarFname)) { logWarning("Package lists tarball '{}' does not exist.", listsTarFname.string()); return {}; } ArchiveDecompressor ad; ad.open(listsTarFname.string()); logDebug("Opened: {}", listsTarFname.string()); std::unordered_map> pkgsMap; for (const auto &entry : ad.read()) { const auto archPkid = fs::path(entry.fname).parent_path().filename().string(); std::shared_ptr pkg; auto it = pkgsMap.find(archPkid); if (it != pkgsMap.end()) { pkg = it->second; } else { pkg = std::make_shared(); pkgsMap[archPkid] = pkg; } const auto infoBaseName = fs::path(entry.fname).filename().string(); if (infoBaseName == "desc") { // we have the description file, add information to this package ListFile descF; descF.loadData(entry.data); pkg->setName(descF.getEntry("NAME")); pkg->setVersion(descF.getEntry("VERSION")); pkg->setArch(descF.getEntry("ARCH")); pkg->setMaintainer(descF.getEntry("PACKAGER")); pkg->setFilename((pkgRoot / descF.getEntry("FILENAME")).string()); setPkgDescription(std::move(pkg), descF.getEntry("DESC")); } else if (infoBaseName == "files") { // we have the files list ListFile filesF; filesF.loadData(entry.data); const std::string filesRaw = filesF.getEntry("FILES"); if (!filesRaw.empty()) { auto filesList = Utils::splitString(filesRaw, '\n'); // add leading slash to files that don't have one for (auto &file : filesList) { if (!file.starts_with('/')) { file = '/' + file; } } pkg->setContents(filesList); } } } std::vector> result; result.reserve(pkgsMap.size()); for (const auto &[pkgId, pkg] : pkgsMap) { if (!pkg->isValid()) { logWarning("Found invalid package ({})! Skipping it.", pkg->toString()); continue; } result.push_back(pkg); } return result; } std::vector> ArchPackageIndex::packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs) { const std::string id = std::format("{}/{}/{}", suite, section, arch); auto it = m_pkgCache.find(id); if (it == m_pkgCache.end()) { auto pkgs = loadPackages(suite, section, arch); std::vector> packagePtrs; packagePtrs.reserve(pkgs.size()); for (const auto &pkg : pkgs) packagePtrs.push_back(std::static_pointer_cast(pkg)); m_pkgCache[id] = packagePtrs; return packagePtrs; } return it->second; } std::shared_ptr ArchPackageIndex::packageForFile( const std::string &fname, const std::string &suite, const std::string §ion) { // Not implemented for Arch Linux backend return nullptr; } bool ArchPackageIndex::hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) { // For simplicity, always assume changes for Arch Linux // In a real implementation, you'd check modification times return true; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/archlinux/alpkgindex.h000066400000000000000000000041201506754475600244260ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include "../interfaces.h" #include "../../utils.h" #include "alpkg.h" #include "listfile.h" namespace ASGenerator { class ArchPackageIndex : public PackageIndex { public: explicit ArchPackageIndex(const std::string &dir); void release() override; std::vector> packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true) override; std::shared_ptr packageForFile( const std::string &fname, const std::string &suite = "", const std::string §ion = "") override; bool hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) override; private: fs::path m_rootDir; std::unordered_map>> m_pkgCache; void setPkgDescription(std::shared_ptr pkg, const std::string &pkgDesc); std::vector> loadPackages( const std::string &suite, const std::string §ion, const std::string &arch); }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/archlinux/listfile.cpp000066400000000000000000000035041506754475600244530ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "listfile.h" #include "../../utils.h" namespace ASGenerator { ListFile::ListFile() {} void ListFile::loadData(const std::vector &data) { std::string dataStr(data.begin(), data.end()); auto content = Utils::splitString(dataStr, '\n'); std::string blockName; for (const auto &line : content) { if (line.starts_with("%") && line.ends_with("%")) { blockName = line.substr(1, line.length() - 2); continue; } if (line.empty()) { blockName.clear(); continue; } if (!blockName.empty()) { auto it = m_entries.find(blockName); if (it != m_entries.end()) { it->second += "\n" + line; } else { m_entries[blockName] = line; } } } } std::string ListFile::getEntry(const std::string &name) { auto it = m_entries.find(name); if (it != m_entries.end()) return it->second; return {}; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/archlinux/listfile.h000066400000000000000000000022451506754475600241210ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include namespace ASGenerator { class ListFile { public: ListFile(); void loadData(const std::vector &data); std::string getEntry(const std::string &name); private: std::unordered_map m_entries; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/archlinux/meson.build000066400000000000000000000001641506754475600242750ustar00rootroot00000000000000 backend_arch_src = files( 'alpkg.cpp', 'alpkgindex.cpp', 'listfile.cpp', ) backends_src += backend_arch_src appstream-generator-0.10.1/src/backends/debian/000077500000000000000000000000001506754475600213575ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/debian/debpkg.cpp000066400000000000000000000233471506754475600233300ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "debpkg.h" #include #include #include #include #include #include "../../config.h" #include "../../logging.h" #include "../../zarchive.h" #include "../../downloader.h" #include "../../utils.h" namespace ASGenerator { void DebPackageLocaleTexts::setDescription(const std::string &text, const std::string &locale) { std::lock_guard lock(m_mutex); description[locale] = text; } void DebPackageLocaleTexts::setSummary(const std::string &text, const std::string &locale) { std::lock_guard lock(m_mutex); summary[locale] = text; } DebPackage::DebPackage( const std::string &pname, const std::string &pver, const std::string &parch, std::shared_ptr l10nTexts) : m_pkgname(pname), m_pkgver(pver), m_pkgarch(parch), m_contentsRead(false), m_controlArchive(std::make_unique()), m_dataArchive(std::make_unique()) { if (l10nTexts) m_descTexts = std::move(l10nTexts); else m_descTexts = std::make_shared(); updateTmpDirPath(); } DebPackage::~DebPackage() { finish(); } std::string DebPackage::name() const { return m_pkgname; } std::string DebPackage::ver() const { return m_pkgver; } std::string DebPackage::arch() const { return m_pkgarch; } std::string DebPackage::maintainer() const { return m_pkgmaintainer; } const std::unordered_map &DebPackage::description() const { return m_descTexts->description; } const std::unordered_map &DebPackage::summary() const { return m_descTexts->summary; } void DebPackage::setName(const std::string &s) { m_pkgname = s; } void DebPackage::setVersion(const std::string &s) { m_pkgver = s; } void DebPackage::setArch(const std::string &s) { m_pkgarch = s; } void DebPackage::setMaintainer(const std::string &maint) { m_pkgmaintainer = maint; } void DebPackage::setFilename(const std::string &fname) { m_debFname = fname; m_localDebFname.clear(); } void DebPackage::setGst(const GStreamer &gst) { m_gstreamer = gst; } std::optional DebPackage::gst() const { return m_gstreamer; } std::string DebPackage::getFilename() { if (!m_localDebFname.empty()) return m_localDebFname; if (Utils::isRemote(m_debFname)) { std::lock_guard lock(m_mutex); auto &dl = Downloader::get(); const fs::path path = m_tmpDir / fs::path(m_debFname).filename(); dl.downloadFile(m_debFname, path.string()); m_localDebFname = path; return m_localDebFname; } else { m_localDebFname = m_debFname; return m_debFname; } } void DebPackage::updateTmpDirPath() { std::lock_guard lock(m_mutex); const auto &conf = Config::get(); m_tmpDir = conf.getTmpDir() / std::format("{}-{}_{}", name(), ver(), arch()); } void DebPackage::setDescription(const std::string &text, const std::string &locale) { m_descTexts->setDescription(text, locale); } void DebPackage::setSummary(const std::string &text, const std::string &locale) { m_descTexts->setSummary(text, locale); } void DebPackage::setLocalizedTexts(std::shared_ptr l10nTexts) { assert(l10nTexts != nullptr); m_descTexts = std::move(l10nTexts); } std::shared_ptr DebPackage::localizedTexts() { return m_descTexts; } ArchiveDecompressor &DebPackage::openPayloadArchive() { if (m_dataArchive->isOpen()) return *m_dataArchive; ArchiveDecompressor ad; // extract the payload to a temporary location first ad.open(getFilename()); fs::create_directories(m_tmpDir); const std::regex dataRegex(R"(data\.*)"); auto files = ad.extractFilesByRegex(dataRegex, m_tmpDir); if (files.empty()) { throw std::runtime_error( std::format("Unable to find the payload tarball in Debian package: {}", getFilename())); } const std::string dataArchiveFname = files[0]; m_dataArchive->open(dataArchiveFname, m_tmpDir / "data"); m_dataArchive->setOptimizeRepeatedReads(true); return *m_dataArchive; } void DebPackage::extractPackage(const std::string &dest) { std::lock_guard lock(m_mutex); fs::path extractPath = dest; if (extractPath.empty()) extractPath = m_tmpDir / name(); if (!fs::exists(extractPath)) fs::create_directories(extractPath); auto &pa = openPayloadArchive(); pa.extractArchive(extractPath); } ArchiveDecompressor &DebPackage::openControlArchive() { { std::lock_guard lock(m_mutex); if (m_controlArchive->isOpen()) return *m_controlArchive; } const auto fname = getFilename(); std::lock_guard lock(m_mutex); ArchiveDecompressor ad; // extract the payload to a temporary location first ad.open(fname); fs::create_directories(m_tmpDir); const std::regex controlRegex(R"(control\.*)"); auto files = ad.extractFilesByRegex(controlRegex, m_tmpDir); if (files.empty()) { throw std::runtime_error(std::format("Unable to find control data in Debian package: {}", getFilename())); } const std::string controlArchiveFname = files[0]; m_controlArchive->open(controlArchiveFname); return *m_controlArchive; } std::vector DebPackage::getFileData(const std::string &fname) { std::lock_guard lock(m_mutex); auto &pa = openPayloadArchive(); return pa.readData(fname); } const std::vector &DebPackage::contents() { { std::lock_guard lock(m_mutex); if (m_contentsRead) return m_contentsL; if (m_pkgname.ends_with("icon-theme")) { // the md5sums file does not contain symbolic links - while that is okay-ish for regular // packages, it is not acceptable for icon themes, since those rely on symlinks to provide // aliases for certain icons. So, use the slow method for reading contents information here. auto &pa = openPayloadArchive(); m_contentsL = pa.readContents(); m_contentsRead = true; return m_contentsL; } } // use the md5sums file of the .deb control archive to determine // the contents of this package. // this is way faster than going through the payload directly, and // has the same accuracy. auto &ca = openControlArchive(); std::vector md5sumsData; try { md5sumsData = ca.readData("./md5sums"); } catch (const std::exception &e) { logWarning("Could not read md5sums file for package {}: {}", id(), e.what()); return m_contentsL; } { std::lock_guard lock(m_mutex); std::string md5sums(md5sumsData.begin(), md5sumsData.end()); m_contentsL.clear(); m_contentsL.reserve(20); const auto lines = Utils::splitString(md5sums, '\n'); for (const auto &line : lines) { // Split on double space - need to use a different approach since Utils::splitString only takes char const auto doublespace = line.find(" "); if (doublespace == std::string::npos || doublespace == 0) continue; // The filename is everything after the first double space const std::string filename = line.substr(doublespace + 2); if (!filename.empty()) { m_contentsL.push_back("/" + filename); } } m_contentsRead = true; return m_contentsL; } } std::unique_ptr DebPackage::readControlInformation() { auto &ca = openControlArchive(); std::vector controlData; try { controlData = ca.readData("./control"); } catch (const std::exception &e) { logError("Could not read control file for package {}: {}", id(), e.what()); return nullptr; } std::string controlStr(controlData.begin(), controlData.end()); auto tf = std::make_unique(); tf->load(controlStr); return tf; } void DebPackage::cleanupTemp() { std::lock_guard lock(m_mutex); if (m_controlArchive->isOpen()) m_controlArchive->close(); if (m_dataArchive->isOpen()) m_dataArchive->close(); if (m_tmpDir.empty()) return; try { if (fs::exists(m_tmpDir)) { /* Whenever we delete the temporary directory, we need to * forget about the local file too, since (if it's remote) that * was downloaded into there. */ m_localDebFname.clear(); fs::remove_all(m_tmpDir); } } catch (const std::exception &e) { // we ignore any error logWarning("Unable to remove temporary directory: {} ({})", m_tmpDir.string(), e.what()); } } void DebPackage::finish() { cleanupTemp(); } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/debian/debpkg.h000066400000000000000000000076321506754475600227740ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include "../interfaces.h" #include "../../utils.h" #include "tagfile.h" namespace ASGenerator { class ArchiveDecompressor; /** * Helper class for simple deduplication of package descriptions * between packages of different architectures in memory. */ class DebPackageLocaleTexts { public: std::unordered_map summary; ///< map of localized package short summaries std::unordered_map description; ///< map of localized package descriptions void setDescription(const std::string &text, const std::string &locale); void setSummary(const std::string &text, const std::string &locale); private: mutable std::mutex m_mutex; }; /** * Representation of a Debian binary package */ class DebPackage : public Package { public: DebPackage( const std::string &pname, const std::string &pver, const std::string &parch, std::shared_ptr l10nTexts = nullptr); ~DebPackage() override; // Package interface implementation std::string name() const override; std::string ver() const override; std::string arch() const override; std::string maintainer() const override; const std::unordered_map &description() const override; const std::unordered_map &summary() const override; std::string getFilename() override; const std::vector &contents() override; std::vector getFileData(const std::string &fname) override; void cleanupTemp() override; void finish() override; std::optional gst() const override; // Debian-specific methods void setName(const std::string &s); void setVersion(const std::string &s); void setArch(const std::string &s); void setMaintainer(const std::string &maint); void setFilename(const std::string &fname); void setGst(const GStreamer &gst); void updateTmpDirPath(); void setDescription(const std::string &text, const std::string &locale); void setSummary(const std::string &text, const std::string &locale); void setLocalizedTexts(std::shared_ptr l10nTexts); std::shared_ptr localizedTexts(); void extractPackage(const std::string &dest = ""); std::unique_ptr readControlInformation(); private: std::string m_pkgname; std::string m_pkgver; std::string m_pkgarch; std::string m_pkgmaintainer; std::shared_ptr m_descTexts; std::optional m_gstreamer; bool m_contentsRead; std::vector m_contentsL; fs::path m_tmpDir; std::unique_ptr m_controlArchive; std::unique_ptr m_dataArchive; std::string m_debFname; fs::path m_localDebFname; mutable std::mutex m_mutex; ArchiveDecompressor &openPayloadArchive(); ArchiveDecompressor &openControlArchive(); }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/debian/debpkgindex.cpp000066400000000000000000000317151506754475600243560ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "debpkgindex.h" #include #include #include #include #include #include "../../config.h" #include "../../logging.h" #include "../../utils.h" #include "../../datastore.h" #include "debutils.h" namespace ASGenerator { DebianPackageIndex::DebianPackageIndex(const std::string &dir) : m_rootDir(dir) { m_pkgCache.clear(); if (!Utils::isRemote(dir) && !fs::exists(dir)) throw std::runtime_error(std::format("Directory '{}' does not exist.", dir)); const auto &conf = Config::get(); m_tmpDir = conf.getTmpDir() / fs::path(dir).filename(); } void DebianPackageIndex::release() { m_pkgCache.clear(); m_l10nTextIndex.clear(); m_indexChanged.clear(); } std::vector DebianPackageIndex::findTranslations(const std::string &suite, const std::string §ion) { const std::string inRelease = (fs::path(m_rootDir) / "dists" / suite / "InRelease").string(); const std::regex translationRegex(std::format(R"({}/i18n/Translation-(\w+)$)", section)); std::unordered_set translations; try { const auto inReleaseContents = Utils::getTextFileContents(inRelease); for (const auto &entry : inReleaseContents) { std::smatch match; if (std::regex_search(entry, match, translationRegex)) translations.insert(match[1].str()); } } catch (const std::exception &ex) { logWarning("Could not get {}, will assume 'en' is available.", inRelease); return {"en"}; } return std::vector(translations.begin(), translations.end()); } std::string DebianPackageIndex::packageDescToAppStreamDesc(const std::vector &lines) { // TODO: We actually need a Markdown-ish parser here if we want // to support listings in package descriptions properly. std::string description = "

"; bool first = true; for (const auto &line : lines) { const auto trimmedLine = Utils::trimString(line); if (trimmedLine == ".") { description += "

\n

"; first = true; continue; } if (first) first = false; else description += " "; description += Utils::escapeXml(Utils::sanitizeUtf8(trimmedLine)); } description += "

"; return description; } void DebianPackageIndex::loadPackageLongDescs( std::unordered_map> &pkgs, const std::string &suite, const std::string §ion) { const auto langs = findTranslations(suite, section); logDebug("Found translations for: {}", Utils::joinStrings(langs, ", ")); for (const auto &lang : langs) { std::string fname; const std::string fullPath = (fs::path("dists") / suite / section / "i18n" / std::format("Translation-{}.{}", lang, "{}")).string(); try { fname = downloadIfNecessary(m_rootDir, m_tmpDir, fullPath); } catch (const std::exception &ex) { logDebug("No translations for {} in {}/{}", lang, suite, section); continue; } TagFile tagf; tagf.open(fname); do { const auto pkgname = tagf.readField("Package"); const auto rawDesc = tagf.readField(std::format("Description-{}", lang)); if (pkgname.empty() || rawDesc.empty()) continue; auto it = pkgs.find(pkgname); if (it == pkgs.end()) continue; auto pkg = it->second; const std::string textPkgId = std::format("{}/{}", pkg->name(), pkg->ver()); std::shared_ptr l10nTexts; auto l10nIt = m_l10nTextIndex.find(textPkgId); if (l10nIt != m_l10nTextIndex.end()) { // we already fetched this information l10nTexts = l10nIt->second; pkg->setLocalizedTexts(l10nTexts); } else { // read new localizations l10nTexts = pkg->localizedTexts(); m_l10nTextIndex[textPkgId] = l10nTexts; } const auto lines = Utils::splitString(rawDesc, '\n'); if (lines.size() < 2) continue; if (lang == "en") l10nTexts->setSummary(lines[0], "C"); l10nTexts->setSummary(lines[0], lang); // Skip the first line (summary) for description std::vector descLines(lines.begin() + 1, lines.end()); const std::string description = packageDescToAppStreamDesc(descLines); if (lang == "en") l10nTexts->setDescription(description, "C"); l10nTexts->setDescription(description, lang); pkg->setLocalizedTexts(std::move(l10nTexts)); } while (tagf.nextSection()); } } std::string DebianPackageIndex::getIndexFile( const std::string &suite, const std::string §ion, const std::string &arch) { const std::string path = (fs::path("dists") / suite / section / std::format("binary-{}", arch)).string(); return downloadIfNecessary(m_rootDir, m_tmpDir, (fs::path(path) / "Packages.{}").string()); } std::shared_ptr DebianPackageIndex::newPackage( const std::string &name, const std::string &ver, const std::string &arch) { return std::make_shared(name, ver, arch); } std::vector> DebianPackageIndex::loadPackages( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs) { auto indexFname = getIndexFile(suite, section, arch); if (!fs::exists(indexFname)) { logWarning("Archive package index file '{}' does not exist.", indexFname); return {}; } TagFile tagf; tagf.open(indexFname); logDebug("Opened: {}", indexFname); std::unordered_map> pkgs; do { const auto name = tagf.readField("Package"); const auto ver = tagf.readField("Version"); const auto fname = tagf.readField("Filename"); const auto pkgArch = tagf.readField("Architecture"); const auto rawDesc = tagf.readField("Description"); if (name.empty()) continue; // sanity check: We only allow arch:all mixed in with packages from other architectures std::string actualArch = (pkgArch != "all") ? arch : pkgArch; auto pkg = newPackage(name, ver, actualArch); pkg->setFilename((fs::path(m_rootDir) / fname).string()); pkg->setMaintainer(tagf.readField("Maintainer")); if (!rawDesc.empty()) { // parse old-style descriptions const auto dSplit = Utils::splitString(rawDesc, '\n'); if (dSplit.size() >= 2) { pkg->setSummary(dSplit[0], "C"); std::vector descLines(dSplit.begin() + 1, dSplit.end()); const std::string description = packageDescToAppStreamDesc(descLines); pkg->setDescription(description, "C"); } } // Parse GStreamer information auto splitAndTrim = [](const std::string &str) -> std::vector { if (str.empty()) return {}; auto parts = Utils::splitString(str, ';'); for (auto &part : parts) { part = Utils::trimString(part); } return parts; }; const auto decoders = splitAndTrim(tagf.readField("Gstreamer-Decoders")); const auto encoders = splitAndTrim(tagf.readField("Gstreamer-Encoders")); const auto elements = splitAndTrim(tagf.readField("Gstreamer-Elements")); const auto uri_sinks = splitAndTrim(tagf.readField("Gstreamer-Uri-Sinks")); const auto uri_sources = splitAndTrim(tagf.readField("Gstreamer-Uri-Sources")); GStreamer gst(decoders, encoders, elements, uri_sinks, uri_sources); if (gst.isNotEmpty()) pkg->setGst(gst); if (!pkg->isValid()) { logWarning("Found invalid package ({})! Skipping it.", pkg->toString()); continue; } // filter out the most recent package version in the packages list auto existingIt = pkgs.find(name); if (existingIt != pkgs.end()) { if (compareVersions(existingIt->second->ver(), pkg->ver()) > 0) continue; } pkgs[name] = std::move(pkg); } while (tagf.nextSection()); // load long descriptions if (withLongDescs) loadPackageLongDescs(pkgs, suite, section); std::vector> result; result.reserve(pkgs.size()); for (const auto &[name, pkg] : pkgs) { result.push_back(pkg); } return result; } std::vector> DebianPackageIndex::packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs) { const std::string id = std::format("{}/{}/{}", suite, section, arch); auto it = m_pkgCache.find(id); if (it == m_pkgCache.end()) { auto pkgs = loadPackages(suite, section, arch, withLongDescs); std::vector> packagePtrs; packagePtrs.reserve(pkgs.size()); for (const auto &pkg : pkgs) { packagePtrs.push_back(std::static_pointer_cast(pkg)); } m_pkgCache[id] = packagePtrs; return packagePtrs; } return it->second; } std::shared_ptr DebianPackageIndex::packageForFile( const std::string &fname, const std::string &suite, const std::string §ion) { auto pkg = newPackage("", "", ""); pkg->setFilename(fname); auto tf = pkg->readControlInformation(); if (!tf) throw std::runtime_error(std::format("Unable to read control information for package {}", fname)); pkg->setName(tf->readField("Package")); pkg->setVersion(tf->readField("Version")); pkg->setArch(tf->readField("Architecture")); if (pkg->name().empty() || pkg->ver().empty() || pkg->arch().empty()) throw std::runtime_error(std::format("Unable to get control data for package {}", fname)); const std::string rawDesc = tf->readField("Description"); const auto dSplit = Utils::splitString(rawDesc, '\n'); if (dSplit.size() >= 2) { pkg->setSummary(dSplit[0], "C"); std::vector descLines(dSplit.begin() + 1, dSplit.end()); const std::string description = packageDescToAppStreamDesc(descLines); pkg->setDescription(description, "C"); } // ensure we have a meaningful temporary directory name pkg->updateTmpDirPath(); return std::static_pointer_cast(pkg); } bool DebianPackageIndex::hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) { auto indexFname = getIndexFile(suite, section, arch); // if the file doesn't exist, we will emit a warning later anyway, so we just ignore this here if (!fs::exists(indexFname)) return true; // check our cache on whether the index had changed auto cacheIt = m_indexChanged.find(indexFname); if (cacheIt != m_indexChanged.end()) return cacheIt->second; const auto mtime = fs::last_write_time(indexFname); const auto currentTime = std::chrono::duration_cast(mtime.time_since_epoch()).count(); auto repoInfo = dstore->getRepoInfo(suite, section, arch); // Update mtime in repo info when we exit this function auto updateRepoInfo = [&]() { repoInfo.data["mtime"] = static_cast(currentTime); dstore->setRepoInfo(suite, section, arch, repoInfo); }; auto mtimeIt = repoInfo.data.find("mtime"); if (mtimeIt == repoInfo.data.end()) { m_indexChanged[indexFname] = true; updateRepoInfo(); return true; } const auto pastTime = std::get(mtimeIt->second); if (pastTime != currentTime) { m_indexChanged[indexFname] = true; updateRepoInfo(); return true; } m_indexChanged[indexFname] = false; updateRepoInfo(); return false; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/debian/debpkgindex.h000066400000000000000000000057611506754475600240250ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include "../interfaces.h" #include "../../utils.h" #include "debpkg.h" namespace ASGenerator { class DebianPackageIndex : public PackageIndex { public: explicit DebianPackageIndex(const std::string &dir); void release() override; std::vector> packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true) override; std::shared_ptr packageForFile( const std::string &fname, const std::string &suite = "", const std::string §ion = "") override; bool hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) override; protected: fs::path m_tmpDir; /** * Convert a Debian package description to a description * that looks nice-ish in AppStream clients. */ std::string packageDescToAppStreamDesc(const std::vector &lines); void loadPackageLongDescs( std::unordered_map> &pkgs, const std::string &suite, const std::string §ion); std::string getIndexFile(const std::string &suite, const std::string §ion, const std::string &arch); virtual std::shared_ptr newPackage( const std::string &name, const std::string &ver, const std::string &arch); std::vector> loadPackages( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true); std::vector findTranslations(const std::string &suite, const std::string §ion); private: std::string m_rootDir; std::unordered_map>> m_pkgCache; // index of localized text for a specific package name std::unordered_map> m_l10nTextIndex; std::unordered_map m_indexChanged; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/debian/debutils.cpp000066400000000000000000000147701506754475600237070ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * Copyright (C) The APT development team. * Copyright (C) 2016 Canonical Ltd * Author(s): Iain Lane * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "debutils.h" #include #include #include #include #include #include #include "../../logging.h" #include "../../downloader.h" #include "../../utils.h" namespace ASGenerator { std::string downloadIfNecessary( const std::string &prefix, const std::string &destPrefix, const std::string &suffix, Downloader *downloader) { if (downloader == nullptr) downloader = &Downloader::get(); const std::vector exts = {"xz", "bz2", "gz"}; for (const auto &ext : exts) { // Replace {} with the extension suffix std::string formattedSuffix = suffix; std::size_t pos = formattedSuffix.find("{}"); if (pos != std::string::npos) formattedSuffix.replace(pos, 2, ext); const std::string fileName = (fs::path(prefix) / formattedSuffix).string(); const std::string destFileName = (fs::path(destPrefix) / (formattedSuffix + "." + ext)).string(); if (Utils::isRemote(fileName)) { try { downloader->downloadFile(fileName, destFileName); return destFileName; } catch (const std::exception &e) { logDebug("Unable to download: {}", e.what()); } } else { if (fs::exists(fileName)) return fileName; } } /* all extensions failed, so we failed */ throw std::runtime_error( std::format("Could not obtain any file matching {}", (fs::path(prefix) / suffix).string())); } /** * This compares a fragment of the version. This is a slightly adapted * version of what dpkg uses in dpkg/lib/dpkg/version.c. * In particular, the a | b = NULL check is removed as we check this in the * caller, we use an explicit end for a | b strings and we check ~ explicit. */ static int order(char c) { if (std::isdigit(c)) return 0; else if (std::isalpha(c)) return c; else if (c == '~') return -1; else if (c) return c + 256; return 0; } /** * Iterate over the whole string * What this does is to split the whole string into groups of * numeric and non numeric portions. For instance: * a67bhgs89 * Has 4 portions 'a', '67', 'bhgs', '89'. A more normal: * 2.7.2-linux-1 * Has '2', '.', '7', '.' ,'-linux-','1' */ static int cmpFragment(const char *a, const char *aEnd, const char *b, const char *bEnd) { const char *lhs = a; const char *rhs = b; while (lhs != aEnd && rhs != bEnd) { int first_diff = 0; while (lhs != aEnd && rhs != bEnd && (!std::isdigit(*lhs) || !std::isdigit(*rhs))) { int vc = order(*lhs); int rc = order(*rhs); if (vc != rc) return vc - rc; ++lhs; ++rhs; } while (*lhs == '0') ++lhs; while (*rhs == '0') ++rhs; while (std::isdigit(*lhs) && std::isdigit(*rhs)) { if (!first_diff) first_diff = *lhs - *rhs; ++lhs; ++rhs; } if (std::isdigit(*lhs)) return 1; if (std::isdigit(*rhs)) return -1; if (first_diff) return first_diff; } // The strings must be equal if (lhs == aEnd && rhs == bEnd) return 0; // lhs is shorter if (lhs == aEnd) { if (*rhs == '~') return 1; return -1; } // rhs is shorter if (rhs == bEnd) { if (*lhs == '~') return -1; return 1; } // Shouldn't happen return 1; } /** * Compare two Debian-style version numbers. */ int compareVersions(const std::string &a, const std::string &b) { const char *ac = a.c_str(); const char *bc = b.c_str(); const char *aEnd = ac + a.length(); const char *bEnd = bc + b.length(); // Strip off the epoch and compare it const char *lhs = static_cast(std::memchr(ac, ':', aEnd - ac)); const char *rhs = static_cast(std::memchr(bc, ':', bEnd - bc)); if (lhs == nullptr) lhs = ac; if (rhs == nullptr) rhs = bc; // Special case: a zero epoch is the same as no epoch, // so remove it. if (lhs != ac) { for (; ac != lhs && *ac == '0'; ++ac) ; if (ac == lhs) { ++lhs; ++ac; } } if (rhs != bc) { for (; bc != rhs && *bc == '0'; ++bc) ; if (bc == rhs) { ++rhs; ++bc; } } // Compare the epoch int res = cmpFragment(ac, lhs, bc, rhs); if (res != 0) return res; // Skip the ':' if (lhs != ac) lhs++; if (rhs != bc) rhs++; // Find the last '-' in the version - use manual reverse search since memrchr isn't standard const char *dlhs = nullptr; const char *drhs = nullptr; // Search backwards for last '-' for (const char *p = aEnd - 1; p >= lhs; --p) { if (*p == '-') { dlhs = p; break; } } for (const char *p = bEnd - 1; p >= rhs; --p) { if (*p == '-') { drhs = p; break; } } if (dlhs == nullptr) dlhs = aEnd; if (drhs == nullptr) drhs = bEnd; // Compare the main version res = cmpFragment(lhs, dlhs, rhs, drhs); if (res != 0) return res; // Skip the '-' if (dlhs != aEnd) dlhs++; if (drhs != bEnd) drhs++; // Compare the revision return cmpFragment(dlhs, aEnd, drhs, bEnd); } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/debian/debutils.h000066400000000000000000000041711506754475600233460ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * Copyright (C) The APT development team. * Copyright (C) 2016 Canonical Ltd * Author(s): Iain Lane * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include namespace ASGenerator { class Downloader; /** * If prefix is remote, download the first of (prefix + suffix).{xz,bz2,gz}, * otherwise check if any of (prefix + suffix).{xz,bz2,gz} exists. * * Returns: Path to the file, which is guaranteed to exist. * * Params: * prefix = First part of the address, i.e. * "http://ftp.debian.org/debian/" or "/srv/mirrors/debian/" * destPrefix = If the file is remote, the directory to save it under, * which is created if necessary. * suffix = the rest of the address, so that (prefix + * suffix).format({xz,bz2,gz}) is a full path or URL, i.e. * "dists/unstable/main/binary-i386/Packages.%s". The suffix must * contain exactly one "%s"; this function is only suitable for * finding `.xz`, `.bz2` and `.gz` files. */ std::string downloadIfNecessary( const std::string &prefix, const std::string &destPrefix, const std::string &suffix, Downloader *downloader = nullptr); /** * Compare two Debian-style version numbers. */ int compareVersions(const std::string &a, const std::string &b); } // namespace ASGenerator appstream-generator-0.10.1/src/backends/debian/meson.build000066400000000000000000000002131506754475600235150ustar00rootroot00000000000000 backend_debian_src = files( 'tagfile.cpp', 'debpkg.cpp', 'debpkgindex.cpp', 'debutils.cpp', ) backends_src += backend_debian_src appstream-generator-0.10.1/src/backends/debian/tagfile.cpp000066400000000000000000000075031506754475600235030ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "tagfile.h" #include #include #include #include #include "../../logging.h" #include "../../zarchive.h" #include "../../utils.h" namespace ASGenerator { TagFile::TagFile() : m_pos(0) { m_currentBlock.clear(); } void TagFile::open(const std::string &fname, bool compressed) { m_fname = fname; if (compressed) { auto data = decompressFile(fname); load(data); } else { std::ifstream file(fname); if (!file.is_open()) throw std::runtime_error(std::format("Could not open file: {}", fname)); std::ostringstream buffer; buffer << file.rdbuf(); load(buffer.str()); } } void TagFile::load(const std::string &data) { m_content = Utils::splitString(data, '\n'); m_pos = 0; readCurrentBlockData(); } void TagFile::first() { m_pos = 0; readCurrentBlockData(); } void TagFile::readCurrentBlockData() { m_currentBlock.clear(); const auto clen = m_content.size(); for (auto i = m_pos; i < clen; i++) { if (m_content[i].empty()) break; // check whether we are in a multiline value field, and just skip forward in that case if (m_content[i].starts_with(" ")) continue; const auto separatorIndex = m_content[i].find(':'); if (separatorIndex == std::string::npos || separatorIndex == 0) continue; auto fieldName = m_content[i].substr(0, separatorIndex); auto fieldData = m_content[i].substr(separatorIndex + 1); // remove whitespace fieldData = Utils::trimString(fieldData); // check if we have a multiline field for (auto j = i + 1; j < clen; j++) { if (m_content[j].empty()) break; if (!m_content[j].starts_with(" ")) break; // we have a multiline field auto data = m_content[j].substr(1); // remove the leading space if (data == ".") fieldData += "\n"; // just a dot means empty line else fieldData += "\n" + data; i = j; // skip forward } m_currentBlock[fieldName] = std::move(fieldData); } } bool TagFile::nextSection() { const auto clen = m_content.size(); // find next section auto i = m_pos; for (; i < clen; i++) { if (m_content[i].empty()) { i++; break; } } if (i >= clen) return false; m_pos = i; readCurrentBlockData(); return !m_currentBlock.empty(); } bool TagFile::eof() const { return m_pos >= m_content.size(); } std::string TagFile::readField(const std::string &fieldName, const std::string &defaultValue) const { const auto it = m_currentBlock.find(fieldName); if (it != m_currentBlock.end()) return it->second; return defaultValue; } bool TagFile::hasField(const std::string &fieldName) const { return m_currentBlock.find(fieldName) != m_currentBlock.end(); } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/debian/tagfile.h000066400000000000000000000033711506754475600231470ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include namespace ASGenerator { /** * Parser for Debian's RFC2822-style metadata. */ class TagFile { private: std::vector m_content; std::size_t m_pos; std::unordered_map m_currentBlock; std::string m_fname; void readCurrentBlockData(); public: TagFile(); void open(const std::string &fname, bool compressed = true); const std::string &fname() const { return m_fname; } void load(const std::string &data); void first(); bool nextSection(); bool eof() const; std::string readField(const std::string &fieldName, const std::string &defaultValue = "") const; bool hasField(const std::string &fieldName) const; const std::unordered_map ¤tBlock() const { return m_currentBlock; } }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/dummy/000077500000000000000000000000001506754475600212705ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/dummy/dummypkg.cpp000066400000000000000000000045571506754475600236440ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "dummypkg.h" #include "../../logging.h" namespace ASGenerator { DummyPackage::DummyPackage(const std::string &pname, const std::string &pver, const std::string &parch) : m_name(pname), m_version(pver), m_arch(parch), m_kind(PackageKind::Physical), m_contents({"NOTHING1", "NOTHING2"}) { } std::string DummyPackage::name() const { return m_name; } std::string DummyPackage::ver() const { return m_version; } std::string DummyPackage::arch() const { return m_arch; } std::string DummyPackage::maintainer() const { return m_maintainer; } const std::unordered_map &DummyPackage::description() const { return m_description; } std::string DummyPackage::getFilename() { return m_testPkgFilename; } void DummyPackage::setFilename(const std::string &fname) { m_testPkgFilename = fname; } void DummyPackage::setMaintainer(const std::string &maint) { m_maintainer = maint; } const std::vector &DummyPackage::contents() { return m_contents; } std::vector DummyPackage::getFileData(const std::string &fname) { if (fname == "TEST") { return {'N', 'O', 'T', 'H', 'I', 'N', 'G'}; } return {}; } void DummyPackage::finish() { // No-op for dummy package } PackageKind DummyPackage::kind() const noexcept { return m_kind; } void DummyPackage::setKind(PackageKind v) noexcept { m_kind = v; } void DummyPackage::setDescription(const std::string &text, const std::string &locale) { m_description[locale] = text; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/dummy/dummypkg.h000066400000000000000000000041331506754475600232770ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include "../interfaces.h" namespace ASGenerator { class DummyPackage : public Package { private: std::string m_name; std::string m_version; std::string m_arch; std::string m_maintainer; std::unordered_map m_description; std::string m_testPkgFilename; PackageKind m_kind; std::vector m_contents; public: DummyPackage(const std::string &pname, const std::string &pver, const std::string &parch); std::string name() const override; std::string ver() const override; std::string arch() const override; std::string maintainer() const override; const std::unordered_map &description() const override; std::string getFilename() override; void setFilename(const std::string &fname); void setMaintainer(const std::string &maint); const std::vector &contents() override; std::vector getFileData(const std::string &fname) override; void finish() override; PackageKind kind() const noexcept override; void setKind(PackageKind v) noexcept; void setDescription(const std::string &text, const std::string &locale); }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/dummy/meson.build000066400000000000000000000001451506754475600234320ustar00rootroot00000000000000 backend_dummy_src = files( 'dummypkg.cpp', 'pkgindex.cpp', ) backends_src += backend_dummy_src appstream-generator-0.10.1/src/backends/dummy/pkgindex.cpp000066400000000000000000000034731506754475600236140ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "pkgindex.h" #include "../../logging.h" namespace ASGenerator { DummyPackageIndex::DummyPackageIndex(const std::string &dir) { // Constructor doesn't need to do anything for dummy backend } void DummyPackageIndex::release() { m_pkgCache.clear(); } std::vector> DummyPackageIndex::packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs) { std::vector> packages; packages.push_back(std::make_shared("test", "1.0", "amd64")); return packages; } std::shared_ptr DummyPackageIndex::packageForFile( const std::string &fname, const std::string &suite, const std::string §ion) { // FIXME: not implemented return nullptr; } bool DummyPackageIndex::hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) { return true; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/dummy/pkgindex.h000066400000000000000000000034061506754475600232550ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include "../interfaces.h" #include "dummypkg.h" namespace ASGenerator { class DummyPackageIndex : public PackageIndex { private: std::unordered_map>> m_pkgCache; public: DummyPackageIndex(const std::string &dir); void release() override; std::vector> packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true) override; std::shared_ptr packageForFile( const std::string &fname, const std::string &suite = "", const std::string §ion = "") override; bool hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) override; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/freebsd/000077500000000000000000000000001506754475600215475ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/freebsd/fbsdpkg.cpp000066400000000000000000000057311506754475600237010ustar00rootroot00000000000000/* * Copyright (C) 2023-2025 Serenity Cyber Security, LLC * Author: Gleb Popov * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "fbsdpkg.h" #include #include #include "../../logging.h" #include "../../zarchive.h" #include "../../config.h" namespace ASGenerator { FreeBSDPackage::FreeBSDPackage(const std::string &pkgRoot, const nlohmann::json &j) : m_pkgjson(j), m_kind(PackageKind::Physical) { m_pkgFname = fs::path(pkgRoot) / m_pkgjson["repopath"].get(); m_pkgArchive = std::make_unique(); } std::string FreeBSDPackage::name() const { return m_pkgjson["name"].get(); } std::string FreeBSDPackage::ver() const { return m_pkgjson["version"].get(); } std::string FreeBSDPackage::arch() const { return m_pkgjson["arch"].get(); } std::string FreeBSDPackage::maintainer() const { return m_pkgjson["maintainer"].get(); } std::string FreeBSDPackage::getFilename() { return m_pkgFname; } const std::unordered_map &FreeBSDPackage::summary() const { if (m_summaryCache.empty()) m_summaryCache["en"] = m_pkgjson["comment"].get(); return m_summaryCache; } const std::unordered_map &FreeBSDPackage::description() const { if (m_descriptionCache.empty()) m_descriptionCache["en"] = m_pkgjson["desc"].get(); return m_descriptionCache; } std::vector FreeBSDPackage::getFileData(const std::string &fname) { std::lock_guard lock(m_mutex); if (!m_pkgArchive->isOpen()) { m_pkgArchive->open(m_pkgFname, Config::get().getTmpDir() / fs::path(m_pkgFname).filename()); m_pkgArchive->setOptimizeRepeatedReads(true); } return m_pkgArchive->readData(fname); } const std::vector &FreeBSDPackage::contents() { if (!m_contentsL.empty()) return m_contentsL; if (!m_pkgArchive->isOpen()) m_pkgArchive->open(getFilename()); m_contentsL = m_pkgArchive->readContents(); return m_contentsL; } void FreeBSDPackage::finish() { // No-op for FreeBSD package } PackageKind FreeBSDPackage::kind() const noexcept { return m_kind; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/freebsd/fbsdpkg.h000066400000000000000000000044221506754475600233420ustar00rootroot00000000000000/* * Copyright (C) 2023-2025 Serenity Cyber Security, LLC * Author: Gleb Popov * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include "../interfaces.h" #include "../../utils.h" namespace ASGenerator { class ArchiveDecompressor; class FreeBSDPackage : public Package { public: FreeBSDPackage(const std::string &pkgRoot, const nlohmann::json &j); ~FreeBSDPackage() override = default; std::string name() const override; std::string ver() const override; std::string arch() const override; std::string maintainer() const override; std::string getFilename() override; const std::unordered_map &summary() const override; const std::unordered_map &description() const override; std::vector getFileData(const std::string &fname) override; const std::vector &contents() override; void finish() override; PackageKind kind() const noexcept override; private: nlohmann::json m_pkgjson; fs::path m_pkgFname; PackageKind m_kind; std::unique_ptr m_pkgArchive; std::vector m_contentsL; // Mutable cache members for lazy initialization of summary/description mutable std::unordered_map m_summaryCache; mutable std::unordered_map m_descriptionCache; mutable std::mutex m_mutex; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/freebsd/fbsdpkgindex.cpp000066400000000000000000000107331506754475600247270ustar00rootroot00000000000000/* * Copyright (C) 2023-2025 Serenity Cyber Security, LLC * Author: Gleb Popov * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "fbsdpkgindex.h" #include #include #include #include "../../logging.h" #include "../../zarchive.h" #include "../../utils.h" namespace ASGenerator { FreeBSDPackageIndex::FreeBSDPackageIndex(const std::string &dir) : m_rootDir(dir) { if (!fs::exists(dir)) throw std::runtime_error(std::format("Directory '{}' does not exist.", dir)); } void FreeBSDPackageIndex::release() { m_pkgCache.clear(); } std::vector> FreeBSDPackageIndex::loadPackages( const std::string &suite, const std::string §ion, const std::string &arch) { const auto pkgRoot = m_rootDir / suite; const auto metaFname = pkgRoot / "meta.conf"; std::string dataFname; if (!fs::exists(metaFname)) { logError("Metadata file '{}' does not exist.", metaFname.string()); return {}; } // Parse meta.conf to find data file name std::ifstream metaFile(metaFname); std::string line; while (std::getline(metaFile, line)) { if (line.starts_with("data")) { // data = "data"; auto splitResult = Utils::splitString(line, '"'); if (splitResult.size() == 3) { dataFname = splitResult[1]; break; } } } const auto dataTarFname = pkgRoot / (dataFname + ".pkg"); if (!fs::exists(dataTarFname)) { logError("Package lists file '{}' does not exist.", dataTarFname.string()); return {}; } ArchiveDecompressor ad; ad.open(dataTarFname.string()); logDebug("Opened: {}", dataTarFname.string()); const auto jsonData = ad.readData(dataFname); const std::string jsonString(jsonData.begin(), jsonData.end()); nlohmann::json dataJson; try { dataJson = nlohmann::json::parse(jsonString); } catch (const std::exception &e) { logError("Failed to parse JSON from '{}': {}", dataTarFname.string(), e.what()); return {}; } if (!dataJson.is_object()) { logError("JSON from '{}' is not an object.", dataTarFname.string()); return {}; } std::vector> packages; if (dataJson.contains("packages") && dataJson["packages"].is_array()) { for (const auto &entry : dataJson["packages"]) { if (entry.is_object()) { auto pkg = std::make_shared(pkgRoot.string(), entry); packages.push_back(std::static_pointer_cast(pkg)); } } } return packages; } std::vector> FreeBSDPackageIndex::packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs) { const std::string id = std::format("{}-{}-{}", suite, section, arch); // Thread-safe cache access std::lock_guard lock(m_cacheMutex); auto it = m_pkgCache.find(id); if (it == m_pkgCache.end()) { auto pkgs = loadPackages(suite, section, arch); m_pkgCache[id] = pkgs; return pkgs; } return it->second; } std::shared_ptr FreeBSDPackageIndex::packageForFile( const std::string &fname, const std::string &suite, const std::string §ion) { // Not implemented for FreeBSD backend return nullptr; } bool FreeBSDPackageIndex::hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) { // For simplicity, always assume changes for FreeBSD // In a real implementation, you'd check modification times of meta.conf and data files return true; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/freebsd/fbsdpkgindex.h000066400000000000000000000042071506754475600243730ustar00rootroot00000000000000/* * Copyright (C) 2023-2025 Serenity Cyber Security, LLC * Author: Gleb Popov * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include #include "../interfaces.h" #include "../../utils.h" #include "fbsdpkg.h" namespace ASGenerator { class FreeBSDPackageIndex : public PackageIndex { public: explicit FreeBSDPackageIndex(const std::string &dir); void release() override; std::vector> packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true) override; std::shared_ptr packageForFile( const std::string &fname, const std::string &suite = "", const std::string §ion = "") override; bool hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) override; private: fs::path m_rootDir; std::unordered_map>> m_pkgCache; std::mutex m_cacheMutex; // Thread safety for cache access std::vector> loadPackages( const std::string &suite, const std::string §ion, const std::string &arch); }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/freebsd/meson.build000066400000000000000000000001541506754475600237110ustar00rootroot00000000000000 backend_freebsd_src = files( 'fbsdpkg.cpp', 'fbsdpkgindex.cpp', ) backends_src += backend_freebsd_src appstream-generator-0.10.1/src/backends/interfaces.cpp000066400000000000000000000050221506754475600227630ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "interfaces.h" #include #include namespace ASGenerator { // GStreamer implementation GStreamer::GStreamer() : m_decoders(), m_encoders(), m_elements(), m_uriSinks(), m_uriSources() { } GStreamer::GStreamer( const std::vector &decoders, const std::vector &encoders, const std::vector &elements, const std::vector &uriSinks, const std::vector &uriSources) : m_decoders(decoders), m_encoders(encoders), m_elements(elements), m_uriSinks(uriSinks), m_uriSources(uriSources) { } bool GStreamer::isNotEmpty() const noexcept { return !( m_decoders.empty() && m_encoders.empty() && m_elements.empty() && m_uriSinks.empty() && m_uriSources.empty()); } PackageKind Package::kind() const noexcept { return PackageKind::Physical; } const std::unordered_map &Package::summary() const { static const std::unordered_map empty_map; return empty_map; } std::optional Package::gst() const { return std::nullopt; } std::unordered_map Package::getDesktopFileTranslations( GKeyFile *desktopFile, const std::string &text) { return {}; } bool Package::hasDesktopFileTranslations() const { return false; } // Package implementation const std::string &Package::id() const { if (m_pkid.empty()) m_pkid = std::format("{}/{}/{}", name(), ver(), arch()); return m_pkid; } bool Package::isValid() const { return !name().empty() && !ver().empty() && !arch().empty(); } std::string Package::toString() const { return id(); } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/interfaces.h000066400000000000000000000167171506754475600224450ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include namespace ASGenerator { class DataStore; class GStreamer { private: std::vector m_decoders; std::vector m_encoders; std::vector m_elements; std::vector m_uriSinks; std::vector m_uriSources; public: GStreamer(); GStreamer( const std::vector &decoders, const std::vector &encoders, const std::vector &elements, const std::vector &uriSinks, const std::vector &uriSources); bool isNotEmpty() const noexcept; const std::vector &decoders() const noexcept { return m_decoders; } const std::vector &encoders() const noexcept { return m_encoders; } const std::vector &elements() const noexcept { return m_elements; } const std::vector &uriSinks() const noexcept { return m_uriSinks; } const std::vector &uriSources() const noexcept { return m_uriSources; } }; /** * Type of a package that can be processed. * Allows distinguishing "real" packages from * virtual or fake ones that are used internally. */ enum class PackageKind { Unknown, Physical, Fake }; /** * Represents a distribution package in the generator. */ class Package { private: mutable std::string m_pkid; public: virtual ~Package() = default; virtual std::string name() const = 0; virtual std::string ver() const = 0; virtual std::string arch() const = 0; virtual std::string maintainer() const = 0; /** * Type of this package (whether it actually exists or is a fake/virtual package) * You pretty much always want PHYSICAL. */ virtual PackageKind kind() const noexcept; /** * A associative array containing package descriptions. * Key is the language (or locale), value the description. * * E.g.: {"en": "A description.", "de": "Eine Beschreibung"} */ virtual const std::unordered_map &description() const = 0; /** * A associative array containing package summaries. * Key is the language (or locale), value the summary. * * E.g.: {"en": "foo the bar"} */ virtual const std::unordered_map &summary() const; /** * Local filename of the package. This string is only used for * issue reporting and other information, the file is never * accessed directly (all data is retrieved via getFileData()). * * This function should return a local filepath, backends might * download missing packages on-demand from a web location. */ virtual std::string getFilename() = 0; /** * A list payload files this package contains. */ virtual const std::vector &contents() = 0; /** * Obtain data for a specific file in the package. */ virtual std::vector getFileData(const std::string &fname) = 0; /** * Remove temporary data that might have been created while loading information from * this package. This function can be called to avoid excessive use of disk space. * As opposed to `finish()`, the package may be reopened afterwards. */ virtual void cleanupTemp() {} /** * Close the package. This function is called when we will * no longer request any file data from this package. */ virtual void finish() = 0; virtual std::optional gst() const; /** * Retrieve backend-specific translations. * * (currently only used by the Ubuntu backend) */ virtual std::unordered_map getDesktopFileTranslations( GKeyFile *desktopFile, const std::string &text); virtual bool hasDesktopFileTranslations() const; /** * Get the unique identifier for this package. * The ID is supposed to be unique per backend, it should never appear * multiple times in suites/sections. */ const std::string &id() const; /** * Check if the package is valid. * A Package must at least have a name, version and architecture defined. */ bool isValid() const; std::string toString() const; // Delete copy constructor and assignment operator Package(const Package &) = delete; Package &operator=(const Package &) = delete; protected: Package() = default; }; /** * An index of information about packages in a distribution. */ class PackageIndex { public: virtual ~PackageIndex() = default; /** * Called after a set of operations has completed, which allows the index to * release memory it might have allocated for cached data, or delete temporary * files. **/ virtual void release() = 0; /** * Get a list of packages for the given suite/section/arch triplet. * The PackageIndex should cache the data if obtaining it is an expensive * operation, since the generator might query the data multiple times. **/ virtual std::vector> packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true) = 0; /** * Get an abstract package representation for a physical package * file. A suite name and section name is obviously given. * This function is used in case only processing of one particular * package is requested. * Backends should return null if the feature is not implemented. **/ virtual std::shared_ptr packageForFile( const std::string &fname, const std::string &suite = "", const std::string §ion = "") = 0; /** * Check if the index for the given suite/section/arch triplet has changed since * the last generator run. The index can use the (get/set)RepoInfo methods on DataStore * to store mtime or checksum data for the given suite. * For the lifetime of the PackageIndex, this method must return the same result, * which means an internal cache is useful. */ virtual bool hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) = 0; // Delete copy constructor and assignment operator PackageIndex(const PackageIndex &) = delete; PackageIndex &operator=(const PackageIndex &) = delete; protected: PackageIndex() = default; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/meson.build000066400000000000000000000002231506754475600222740ustar00rootroot00000000000000 backends_src = [] subdir('dummy') subdir('debian') subdir('ubuntu') subdir('alpinelinux') subdir('archlinux') subdir('rpmmd') subdir('freebsd') appstream-generator-0.10.1/src/backends/rpmmd/000077500000000000000000000000001506754475600212545ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/rpmmd/meson.build000066400000000000000000000001701506754475600234140ustar00rootroot00000000000000 backend_rpmmd_src = files( 'rpmpkg.cpp', 'rpmpkgindex.cpp', 'rpmutils.cpp', ) backends_src += backend_rpmmd_src appstream-generator-0.10.1/src/backends/rpmmd/rpmpkg.cpp000066400000000000000000000076461506754475600232750ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "rpmpkg.h" #include #include #include "../../config.h" #include "../../logging.h" #include "../../zarchive.h" #include "../../downloader.h" #include "../../utils.h" namespace ASGenerator { RPMPackage::RPMPackage() : m_archive(std::make_unique()) { } std::string RPMPackage::name() const { return m_pkgname; } void RPMPackage::setName(const std::string &val) { m_pkgname = val; } std::string RPMPackage::ver() const { return m_pkgver; } void RPMPackage::setVersion(const std::string &val) { m_pkgver = val; } std::string RPMPackage::arch() const { return m_pkgarch; } void RPMPackage::setArch(const std::string &val) { m_pkgarch = val; } const std::unordered_map &RPMPackage::description() const { return m_desc; } const std::unordered_map &RPMPackage::summary() const { return m_summ; } std::string RPMPackage::getFilename() { if (!m_localPkgFname.empty()) return m_localPkgFname; if (Utils::isRemote(m_pkgFname)) { std::lock_guard lock(m_mutex); const auto &conf = Config::get(); auto &dl = Downloader::get(); const fs::path path = conf.getTmpDir() / std::format( "{}-{}_{}_{}", name(), ver(), arch(), fs::path(m_pkgFname).filename().string()); dl.downloadFile(m_pkgFname, path); m_localPkgFname = path; return m_localPkgFname; } else { m_localPkgFname = m_pkgFname; return m_pkgFname; } } void RPMPackage::setFilename(const std::string &fname) { m_pkgFname = fname; } std::string RPMPackage::maintainer() const { return m_pkgmaintainer; } void RPMPackage::setMaintainer(const std::string &maint) { m_pkgmaintainer = maint; } void RPMPackage::setDescription(const std::string &text, const std::string &locale) { m_desc[locale] = text; } void RPMPackage::setSummary(const std::string &text, const std::string &locale) { m_summ[locale] = text; } std::vector RPMPackage::getFileData(const std::string &fname) { std::lock_guard lock(m_mutex); if (!m_archive->isOpen()) { const auto pkgFilename = getFilename(); m_archive->open(pkgFilename, Config::get().getTmpDir() / fs::path(pkgFilename).filename()); m_archive->setOptimizeRepeatedReads(true); } return m_archive->readData(fname); } const std::vector &RPMPackage::contents() { return m_contentsL; } void RPMPackage::setContents(const std::vector &c) { m_contentsL = c; } void RPMPackage::finish() { std::lock_guard lock(m_mutex); if (m_archive->isOpen()) m_archive->close(); try { if (Utils::isRemote(m_pkgFname) && fs::exists(m_localPkgFname)) { fs::remove(m_localPkgFname); m_localPkgFname.clear(); } } catch (const std::exception &e) { // we ignore any error logDebug("Unable to remove temporary package: {} ({})", m_localPkgFname.string(), e.what()); } } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/rpmmd/rpmpkg.h000066400000000000000000000050031506754475600227230ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include "../interfaces.h" #include "../../utils.h" namespace ASGenerator { class ArchiveDecompressor; class RPMPackage : public Package { public: RPMPackage(); ~RPMPackage() override = default; std::string name() const override; void setName(const std::string &val); std::string ver() const override; void setVersion(const std::string &val); std::string arch() const override; void setArch(const std::string &val); const std::unordered_map &description() const override; const std::unordered_map &summary() const override; std::string getFilename() override; void setFilename(const std::string &fname); std::string maintainer() const override; void setMaintainer(const std::string &maint); void setDescription(const std::string &text, const std::string &locale); void setSummary(const std::string &text, const std::string &locale); std::vector getFileData(const std::string &fname) override; const std::vector &contents() override; void setContents(const std::vector &c); void finish() override; private: std::string m_pkgname; std::string m_pkgver; std::string m_pkgarch; std::string m_pkgmaintainer; std::unordered_map m_desc; std::unordered_map m_summ; std::string m_pkgFname; fs::path m_localPkgFname; std::vector m_contentsL; std::unique_ptr m_archive; mutable std::mutex m_mutex; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/rpmmd/rpmpkgindex.cpp000066400000000000000000000314231506754475600243130ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "rpmpkgindex.h" #include #include #include #include #include #include "../../config.h" #include "../../logging.h" #include "../../utils.h" #include "../../downloader.h" #include "../../zarchive.h" #include "rpmutils.h" namespace ASGenerator { RPMPackageIndex::RPMPackageIndex(const std::string &dir) : m_rootDir(dir) { if (!Utils::isRemote(dir) && !fs::exists(dir)) throw std::runtime_error(std::format("Directory '{}' does not exist.", dir)); const auto &conf = Config::get(); m_tmpRootDir = conf.getTmpDir() / fs::path(dir).filename(); ; } RPMPackageIndex::~RPMPackageIndex() = default; void RPMPackageIndex::release() { m_pkgCache.clear(); } static std::string getXmlStrAttr(xmlNodePtr elem, const std::string &name) { if (!elem || !elem->properties) return {}; for (xmlAttrPtr attr = elem->properties; attr; attr = attr->next) { if (attr->name && std::strcmp(reinterpret_cast(attr->name), name.c_str()) == 0) { if (attr->children && attr->children->content) return reinterpret_cast(attr->children->content); } } return {}; } static std::string getXmlElemText(xmlNodePtr elem) { if (!elem) return {}; for (xmlNodePtr child = elem->children; child; child = child->next) { if (child->type == XML_TEXT_NODE && child->content) return reinterpret_cast(child->content); } return {}; } std::vector> RPMPackageIndex::loadPackages( const std::string &suite, const std::string §ion, const std::string &arch) { // IMPORTANT: This function is *not* thread-safe! The caller needs to ensure thread-safety. const auto repoRoot = m_rootDir / suite / section / arch / "os"; std::vector primaryIndexFiles; std::vector filelistFiles; // Download and parse repomd.xml const auto repoMdFname = downloadIfNecessary((repoRoot / "repodata" / "repomd.xml").string(), m_tmpRootDir); std::ifstream repoMdFile(repoMdFname); if (!repoMdFile.is_open()) { logError("Could not open repomd.xml file: {}", repoMdFname); return {}; } std::string repoMdContent((std::istreambuf_iterator(repoMdFile)), std::istreambuf_iterator()); // Parse index data xmlDocPtr doc = xmlParseMemory(repoMdContent.c_str(), static_cast(repoMdContent.length())); if (!doc) { logError("Failed to parse repomd.xml"); return {}; } xmlNodePtr root = xmlDocGetRootElement(doc); if (!root) { xmlFreeDoc(doc); logError("No root element in repomd.xml"); return {}; } // Find primary and filelists data locations for (xmlNodePtr node = root->children; node; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (std::strcmp(reinterpret_cast(node->name), "data") == 0) { std::string dataType = getXmlStrAttr(node, "type"); for (xmlNodePtr child = node->children; child; child = child->next) { if (child->type != XML_ELEMENT_NODE) continue; if (std::strcmp(reinterpret_cast(child->name), "location") == 0) { std::string href = getXmlStrAttr(child, "href"); if (!href.empty()) { if (dataType == "primary") primaryIndexFiles.push_back(std::move(href)); else if (dataType == "filelists") filelistFiles.push_back(std::move(href)); } } } } } xmlFreeDoc(doc); if (primaryIndexFiles.empty()) { logWarning("No primary metadata found in repomd.xml"); return {}; } // package-id -> RPMPackage std::unordered_map> pkgMap; // Parse primary metadata for (const auto &primaryFile : primaryIndexFiles) { const auto metaFname = downloadIfNecessary((repoRoot / primaryFile).string(), m_tmpRootDir); std::string data; if (primaryFile.ends_with(".xml")) { std::ifstream primaryStream(metaFname); if (!primaryStream.is_open()) { logWarning("Could not open primary metadata file: {}", metaFname); continue; } data = std::string((std::istreambuf_iterator(primaryStream)), std::istreambuf_iterator()); } else { // Handle compressed files using existing decompression utility data = decompressFile(metaFname); } xmlDocPtr primaryDoc = xmlParseMemory(data.c_str(), static_cast(data.length())); if (!primaryDoc) { logError("Failed to parse primary metadata XML"); continue; } xmlNodePtr primaryRoot = xmlDocGetRootElement(primaryDoc); if (!primaryRoot) { xmlFreeDoc(primaryDoc); continue; } // Parse package entries for (xmlNodePtr pkgElem = primaryRoot->children; pkgElem; pkgElem = pkgElem->next) { if (pkgElem->type != XML_ELEMENT_NODE) continue; if (std::strcmp(reinterpret_cast(pkgElem->name), "package") == 0) { // Check package type if (getXmlStrAttr(pkgElem, "type") != "rpm") continue; auto pkg = std::make_shared(); pkg->setMaintainer("None"); // Default maintainer std::string pkgidCS; // Package ID checksum (critical for matching) // Parse package children for (xmlNodePtr child = pkgElem->children; child; child = child->next) { if (child->type != XML_ELEMENT_NODE) continue; const char *childName = reinterpret_cast(child->name); if (std::strcmp(childName, "name") == 0) { pkg->setName(getXmlElemText(child)); } else if (std::strcmp(childName, "arch") == 0) { pkg->setArch(getXmlElemText(child)); } else if (std::strcmp(childName, "summary") == 0) { pkg->setSummary(getXmlElemText(child), "C"); } else if (std::strcmp(childName, "description") == 0) { pkg->setDescription(getXmlElemText(child), "C"); } else if (std::strcmp(childName, "packager") == 0) { pkg->setMaintainer(getXmlElemText(child)); } else if (std::strcmp(childName, "version") == 0) { std::string epoch = getXmlStrAttr(child, "epoch"); std::string upstream_ver = getXmlStrAttr(child, "ver"); std::string rel = getXmlStrAttr(child, "rel"); std::string version; if (epoch.empty() || epoch == "0") version = std::format("{}-{}", upstream_ver, rel); else version = std::format("{}:{}-{}", epoch, upstream_ver, rel); pkg->setVersion(version); } else if (std::strcmp(childName, "location") == 0) { std::string href = getXmlStrAttr(child, "href"); if (!href.empty()) pkg->setFilename((repoRoot / href).string()); } else if (std::strcmp(childName, "checksum") == 0) { if (getXmlStrAttr(child, "pkgid") == "YES") pkgidCS = getXmlElemText(child); } } if (pkgidCS.empty()) { logWarning( "Found package '{}' in '{}' without suitable pkgid. Ignoring it.", pkg->name(), primaryFile); continue; } pkgMap[pkgidCS] = std::move(pkg); } } xmlFreeDoc(primaryDoc); } // Parse filelists metadata for (const auto &filelistFile : filelistFiles) { const auto flistFname = downloadIfNecessary((repoRoot / filelistFile).string(), m_tmpRootDir); std::string data; if (filelistFile.ends_with(".xml")) { std::ifstream filelistStream(flistFname); if (!filelistStream.is_open()) { logWarning("Could not open filelist metadata file: {}", flistFname); continue; } data = std::string((std::istreambuf_iterator(filelistStream)), std::istreambuf_iterator()); } else { // Handle compressed files using existing decompression utility data = decompressFile(flistFname); } xmlDocPtr flDoc = xmlParseMemory(data.c_str(), static_cast(data.length())); if (!flDoc) { logError("Failed to parse filelist metadata XML"); continue; } xmlNodePtr flRoot = xmlDocGetRootElement(flDoc); if (!flRoot) { xmlFreeDoc(flDoc); continue; } // Parse package file entries for (xmlNodePtr pkgElem = flRoot->children; pkgElem; pkgElem = pkgElem->next) { if (pkgElem->type != XML_ELEMENT_NODE) continue; if (std::strcmp(reinterpret_cast(pkgElem->name), "package") == 0) { // Get package ID const std::string pkgid = getXmlStrAttr(pkgElem, "pkgid"); auto pkgIt = pkgMap.find(pkgid); if (pkgIt == pkgMap.end()) continue; auto pkg = pkgIt->second; std::vector contents; for (xmlNodePtr fileElem = pkgElem->children; fileElem; fileElem = fileElem->next) { if (fileElem->type == XML_ELEMENT_NODE && std::strcmp(reinterpret_cast(fileElem->name), "file") == 0) { std::string filePath = getXmlElemText(fileElem); if (!filePath.empty()) contents.push_back(std::move(filePath)); } } pkg->setContents(contents); } } xmlFreeDoc(flDoc); } // Convert to vector and return std::vector> packages; packages.reserve(pkgMap.size()); for (const auto &[pkgid, pkg] : pkgMap) packages.push_back(pkg); logDebug("Loaded {} packages from RPM metadata", packages.size()); return packages; } std::vector> RPMPackageIndex::packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs) { const std::string id = std::format("{}-{}-{}", suite, section, arch); // Thread-safe cache access std::lock_guard lock(m_cacheMutex); auto it = m_pkgCache.find(id); if (it == m_pkgCache.end()) { auto pkgs = loadPackages(suite, section, arch); std::vector> packagePtrs; packagePtrs.reserve(pkgs.size()); for (const auto &pkg : pkgs) packagePtrs.push_back(std::static_pointer_cast(pkg)); m_pkgCache[id] = packagePtrs; return packagePtrs; } return it->second; } std::shared_ptr RPMPackageIndex::packageForFile( const std::string &fname, const std::string &suite, const std::string §ion) { // FIXME: Not implemented for RPM MD backend return nullptr; } bool RPMPackageIndex::hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) { // FIXME: We currently always assume changes for RPM MD... return true; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/rpmmd/rpmpkgindex.h000066400000000000000000000043711506754475600237620ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include #include "../interfaces.h" #include "rpmpkg.h" namespace fs = std::filesystem; namespace ASGenerator { class RPMPackageIndex : public PackageIndex { public: explicit RPMPackageIndex(const std::string &dir); ~RPMPackageIndex(); void release() override; std::vector> packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true) override; std::shared_ptr packageForFile( const std::string &fname, const std::string &suite = "", const std::string §ion = "") override; bool hasChanges( std::shared_ptr dstore, const std::string &suite, const std::string §ion, const std::string &arch) override; private: fs::path m_rootDir; fs::path m_tmpRootDir; std::unordered_map>> m_pkgCache; mutable std::mutex m_cacheMutex; // Thread safety for cache access void setPkgDescription(std::shared_ptr pkg, const std::string &pkgDesc); std::vector> loadPackages( const std::string &suite, const std::string §ion, const std::string &arch); }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/rpmmd/rpmutils.cpp000066400000000000000000000035211506754475600236400ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "rpmutils.h" #include #include #include "../../logging.h" #include "../../downloader.h" #include "../../utils.h" namespace fs = std::filesystem; namespace ASGenerator { std::string downloadIfNecessary(const std::string &url, const std::string &destLocation, Downloader *downloader) { if (downloader == nullptr) downloader = &Downloader::get(); if (Utils::isRemote(url)) { const std::string destFileName = (fs::path(destLocation) / fs::path(url).filename()).string(); try { fs::create_directories(destLocation); downloader->downloadFile(url, destFileName); return destFileName; } catch (const std::exception &e) { logDebug("Unable to download: {}", e.what()); throw std::runtime_error(std::format("Could not obtain file {}", url)); } } else { if (fs::exists(url)) return url; } throw std::runtime_error(std::format("Could not obtain file {}", url)); } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/rpmmd/rpmutils.h000066400000000000000000000026611506754475600233110ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include namespace ASGenerator { class Downloader; /** * If URL is remote, download it, otherwise use it verbatim. * * Returns: Path to the file, which is guaranteed to exist. * * Params: * url = First part of the address, i.e. * "http://ftp.debian.org/debian/" or "/srv/mirrors/debian/" * destLocation = If the file is remote, the directory to save it under, * which is created if necessary. */ std::string downloadIfNecessary( const std::string &url, const std::string &destLocation, Downloader *downloader = nullptr); } // namespace ASGenerator appstream-generator-0.10.1/src/backends/ubuntu/000077500000000000000000000000001506754475600214575ustar00rootroot00000000000000appstream-generator-0.10.1/src/backends/ubuntu/meson.build000066400000000000000000000001501506754475600236150ustar00rootroot00000000000000 backend_ubuntu_src = files( 'ubupkg.cpp', 'ubupkgindex.cpp', ) backends_src += backend_ubuntu_src appstream-generator-0.10.1/src/backends/ubuntu/ubupkg.cpp000066400000000000000000000245011506754475600234620ustar00rootroot00000000000000/* * Copyright (C) 2016-2020 Canonical Ltd * Author: Iain Lane * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "ubupkg.h" #include #include #include #include #include #include #include #include #include #include #include #include "../../logging.h" #include "../../utils.h" namespace ASGenerator { LanguagePackProvider::LanguagePackProvider(const fs::path &globalTmpDir) : m_globalTmpDir(globalTmpDir), m_langpackDir(globalTmpDir / "langpacks"), m_localeDir(m_langpackDir / "locales") { g_autofree gchar *localedefExe = g_find_program_in_path("localedef"); if (localedefExe) m_localedefExe = localedefExe; if (m_localedefExe.empty()) logWarning("localedef executable not found in PATH"); } void LanguagePackProvider::addLanguagePacks(const std::vector> &langpacks) { std::lock_guard lock(m_mutex); m_langpacks.insert(m_langpacks.end(), langpacks.begin(), langpacks.end()); } void LanguagePackProvider::clear() { std::lock_guard lock(m_mutex); m_langpacks.clear(); } void LanguagePackProvider::extractLangpacks() { if (fs::exists(m_langpackDir)) return; // Already extracted std::unordered_set extracted; fs::create_directories(m_langpackDir); for (auto &pkg : m_langpacks) { if (extracted.contains(pkg->name())) continue; logDebug("Extracting {}", pkg->name()); pkg->extractPackage(m_langpackDir); extracted.insert(pkg->name()); } fs::create_directories(m_localeDir); if (extracted.empty()) { logWarning("We have extracted no language packs for this repository!"); m_langpackLocales.clear(); m_langpacks.clear(); return; } // Process supported locales const auto supportedDir = m_langpackDir / "var" / "lib" / "locales" / "supported.d"; if (!fs::exists(supportedDir)) { logWarning("No supported locales directory found in language packs"); return; } // Collect all locale files first std::vector localeFiles; for (const auto &entry : fs::directory_iterator(supportedDir)) { if (entry.is_regular_file()) localeFiles.push_back(entry.path()); } // Process locale files in parallel (batch size of 5) tbb::parallel_for( tbb::blocked_range(0, localeFiles.size(), 5), [&](const tbb::blocked_range &range) { for (std::size_t i = range.begin(); i != range.end(); ++i) { const auto &localeFile = localeFiles[i]; std::ifstream file(localeFile); std::string line; while (std::getline(file, line)) { line = Utils::trimString(line); if (line.empty()) continue; const auto components = Utils::splitString(line, ' '); if (components.size() < 2) continue; const auto localeCharset = Utils::splitString(components[0], '.'); if (localeCharset.empty()) continue; const auto outdir = fs::path(m_localeDir) / components[0]; if (m_localedefExe.empty()) { logWarning("Not generating locale {}: The localedef binary is missing.", components[0]); continue; } logDebug("Generating locale in {}", outdir.string()); // Execute localedef to generate locale std::vector args = { m_localedefExe, "--no-archive", "-i", localeCharset[0], "-c", "-f", components[1], outdir.string()}; // Convert to char* array for g_spawn_sync std::vector argv; argv.reserve(args.size() + 1); for (auto &arg : args) argv.push_back(arg.data()); argv.push_back(nullptr); g_autoptr(GError) error = nullptr; gint exit_status = 0; gboolean success = g_spawn_sync( nullptr, // working_directory argv.data(), // argv nullptr, // envp G_SPAWN_DEFAULT, // flags nullptr, // child_setup nullptr, // user_data nullptr, // standard_output nullptr, // standard_error &exit_status, // exit_status &error // error ); if (!success || exit_status != 0) { if (error) logDebug( "Failed to generate locale for {}: {}", components[0], std::string{error->message}); else logDebug( "Failed to generate locale for {} (exit status: {})", components[0], static_cast(exit_status)); } } } }, tbb::static_partitioner{}); // Clear langpacks as we don't need them in memory anymore after extraction m_langpacks.clear(); // Collect available locales if (m_langpackLocales.empty() && fs::exists(m_localeDir)) { for (const auto &entry : fs::directory_iterator(m_localeDir)) { if (entry.is_directory()) m_langpackLocales.push_back(entry.path().filename().string()); } } } std::unordered_map LanguagePackProvider::getTranslationsPrivate( const std::string &domain, const std::string &text) { // Store original environment variables std::unordered_map originalEnv; static const std::vector envVars = {"LC_ALL", "LANG", "LANGUAGE", "LC_MESSAGES"}; for (const auto &var : envVars) { const char *value = std::getenv(var.c_str()); if (value) { originalEnv[var] = value; unsetenv(var.c_str()); } } // Restore environment on exit auto restoreEnv = [&originalEnv]() { for (const auto &[key, value] : originalEnv) setenv(key.c_str(), value.c_str(), 1); }; // Set locale path setenv("LOCPATH", m_localeDir.c_str(), 1); // Store original locale const char *originalLocale = setlocale(LC_ALL, ""); std::string origLocaleStr = originalLocale ? originalLocale : "C"; const auto translationDir = m_langpackDir / "usr" / "share" / "locale-langpack"; std::unordered_map result; for (const auto &locale : m_langpackLocales) { setlocale(LC_ALL, locale.c_str()); bindtextdomain(domain.c_str(), translationDir.c_str()); const char *translatedText = dgettext(domain.c_str(), text.c_str()); if (translatedText && text != translatedText) result[locale] = translatedText; } // Restore original locale setlocale(LC_ALL, origLocaleStr.c_str()); // Restore environment restoreEnv(); return result; } std::unordered_map LanguagePackProvider::getTranslations( const std::string &domain, const std::string &text) { // this functions does nasty things like changing environment variables and // messing with other global state. We therefore need to ensure that nothing // else is running in parallel static std::mutex global_translation_mutex; std::lock_guard globalLock(global_translation_mutex); // Also lock the instance to prevent concurrent modification of langpacks std::lock_guard instanceLock(m_mutex); extractLangpacks(); return getTranslationsPrivate(domain, text); } UbuntuPackage::UbuntuPackage( const std::string &pname, const std::string &pver, const std::string &parch, std::shared_ptr l10nTexts) : DebPackage(pname, pver, parch, std::move(l10nTexts)) { } void UbuntuPackage::setLanguagePackProvider(std::shared_ptr provider) { m_langpackProvider = std::move(provider); } bool UbuntuPackage::hasDesktopFileTranslations() const { return m_langpackProvider != nullptr; } std::unordered_map UbuntuPackage::getDesktopFileTranslations( GKeyFile *desktopFile, const std::string &text) { if (!m_langpackProvider) return {}; std::string langpackDomain; // Try X-Ubuntu-Gettext-Domain first, then X-GNOME-Gettext-Domain as fallback g_autoptr(GError) error = nullptr; g_autofree gchar *domain = g_key_file_get_string(desktopFile, "Desktop Entry", "X-Ubuntu-Gettext-Domain", &error); if (!domain || error) { g_clear_error(&error); domain = g_key_file_get_string(desktopFile, "Desktop Entry", "X-GNOME-Gettext-Domain", &error); if (!domain || error) return {}; } langpackDomain = domain; logDebug("{} has langpack domain {}", name(), langpackDomain); return m_langpackProvider->getTranslations(langpackDomain, text); } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/ubuntu/ubupkg.h000066400000000000000000000050761506754475600231350ustar00rootroot00000000000000/* * Copyright (C) 2016-2020 Canonical Ltd * Author: Iain Lane * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include "../debian/debpkg.h" namespace ASGenerator { class UbuntuPackage; /** * A helper class that provides functions to work with language packs * used in Ubuntu. */ class LanguagePackProvider { public: explicit LanguagePackProvider(const fs::path &globalTmpDir); void addLanguagePacks(const std::vector> &langpacks); void clear(); std::unordered_map getTranslations(const std::string &domain, const std::string &text); private: std::vector> m_langpacks; fs::path m_globalTmpDir; fs::path m_langpackDir; fs::path m_localeDir; std::string m_localedefExe; std::vector m_langpackLocales; mutable std::mutex m_mutex; void extractLangpacks(); std::unordered_map getTranslationsPrivate( const std::string &domain, const std::string &text); }; /** * Ubuntu package - extends Debian package with language pack support */ class UbuntuPackage : public DebPackage { public: UbuntuPackage( const std::string &pname, const std::string &pver, const std::string &parch, std::shared_ptr l10nTexts = nullptr); void setLanguagePackProvider(std::shared_ptr provider); std::unordered_map getDesktopFileTranslations( GKeyFile *desktopFile, const std::string &text) override; bool hasDesktopFileTranslations() const override; private: std::shared_ptr m_langpackProvider; }; } // namespace ASGenerator appstream-generator-0.10.1/src/backends/ubuntu/ubupkgindex.cpp000066400000000000000000000063611506754475600245160ustar00rootroot00000000000000/* * Copyright (C) 2016-2020 Canonical Ltd * Author: Iain Lane * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "ubupkgindex.h" #include #include #include "../../logging.h" namespace ASGenerator { UbuntuPackageIndex::UbuntuPackageIndex(const std::string &dir) : DebianPackageIndex(dir) { /* * UbuntuPackage needs to extract the langpacks, so we give it an array * of langpacks. There is a small overhead when computing this array * which might be unnecessary if no processed packages are using * langpacks, but otherwise we need to keep a reference to all packages * around, which is very expensive. */ m_langpacks = std::make_shared(m_tmpDir); } void UbuntuPackageIndex::release() { DebianPackageIndex::release(); m_checkedLangPacks.clear(); // replace with fresh, empty provider m_langpacks = std::make_shared(m_tmpDir); } std::shared_ptr UbuntuPackageIndex::newPackage( const std::string &name, const std::string &ver, const std::string &arch) { auto ubuntuPkg = std::make_shared(name, ver, arch); ubuntuPkg->setLanguagePackProvider(m_langpacks); return std::static_pointer_cast(ubuntuPkg); } std::vector> UbuntuPackageIndex::packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs) { auto pkgs = DebianPackageIndex::packagesFor(suite, section, arch, withLongDescs); const std::string ssaId = std::format("{}/{}/{}", suite, section, arch); if (m_checkedLangPacks.contains(ssaId)) return pkgs; // no need to scan for language packs, we already did that // scan for language packs and add them to the data provider std::vector> langpackPkgs; langpackPkgs.reserve(32); for (const auto &pkg : pkgs) { if (pkg->name().starts_with("language-pack-")) { // Cast to UbuntuPackage auto ubuntuPkg = std::dynamic_pointer_cast(pkg); if (ubuntuPkg) langpackPkgs.push_back(std::move(ubuntuPkg)); } } m_langpacks->addLanguagePacks(langpackPkgs); m_checkedLangPacks.insert(ssaId); return pkgs; } std::shared_ptr UbuntuPackageIndex::packageForFile( const std::string &fname, const std::string &suite, const std::string §ion) { // FIXME: not implemented return nullptr; } } // namespace ASGenerator appstream-generator-0.10.1/src/backends/ubuntu/ubupkgindex.h000066400000000000000000000037621506754475600241650ustar00rootroot00000000000000/* * Copyright (C) 2016-2020 Canonical Ltd * Author: Iain Lane * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include "../debian/debpkgindex.h" #include "ubupkg.h" namespace ASGenerator { class UbuntuPackageIndex : public DebianPackageIndex { public: explicit UbuntuPackageIndex(const std::string &dir); void release() override; std::vector> packagesFor( const std::string &suite, const std::string §ion, const std::string &arch, bool withLongDescs = true) override; std::shared_ptr packageForFile( const std::string &fname, const std::string &suite = "", const std::string §ion = "") override; protected: // Make tmpDir accessible to this class using DebianPackageIndex::m_tmpDir; std::shared_ptr newPackage(const std::string &name, const std::string &ver, const std::string &arch) override; private: std::shared_ptr m_langpacks; // holds the IDs of suite/section/arch combinations where we scanned language packs std::unordered_set m_checkedLangPacks; }; } // namespace ASGenerator appstream-generator-0.10.1/src/config.cpp000066400000000000000000000524551506754475600203470ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "defines.h" #include "config.h" #include #include #include #include #include #include #include #include #include #include "logging.h" #include "utils.h" #include "yaml-utils.h" namespace fs = std::filesystem; namespace ASGenerator { // Static member definitions std::unique_ptr Config::instance_; std::once_flag Config::initialized_; Config::Config() : backend(Backend::Unknown), metadataType(DataType::XML), maxScrFileSize(14) { // our default export format version formatVersion = AS_FORMAT_VERSION_V1_0; // find all the external binaries we (may) need // we search for them unconditionally, because the unittests may rely on their absolute // paths being set even if a particular feature flag that requires them isn't. const auto optipngBin_c = asc_globals_get_optipng_binary(); optipngBinary = optipngBin_c ? std::string(optipngBin_c) : ""; g_autofree gchar *ffprobeBin_c = g_find_program_in_path("ffprobe"); ffprobeBinary = ffprobeBin_c ? std::string(ffprobeBin_c) : ""; // new default icon policy instance m_iconPolicy = asc_icon_policy_new(); } Config::~Config() { if (m_iconPolicy) g_object_unref(m_iconPolicy); } Config &Config::get() { std::call_once(initialized_, []() { instance_ = std::unique_ptr(new Config()); }); return *instance_; } std::string Config::formatVersionStr() const { return as_format_version_to_string(formatVersion); } fs::path Config::databaseDir() const { return m_workspaceDir / "db"; } fs::path Config::cacheRootDir() const { return m_workspaceDir / "cache"; } fs::path Config::templateDir() const { // find a suitable template directory // first check the workspace auto tdir = m_workspaceDir / "templates"; tdir = getVendorTemplateDir(tdir, true); if (tdir.empty()) tdir = getVendorTemplateDir(Utils::getDataPath("templates")); return tdir; } AscIconPolicy *Config::iconPolicy() const { return m_iconPolicy; } /** * Helper function to determine a vendor template directory. */ fs::path Config::getVendorTemplateDir(const std::string &dir, bool allowRoot) const { if (!projectName.empty()) { auto tdir = (fs::path(dir) / Utils::toLower(projectName)).string(); if (Utils::existsAndIsDir(tdir)) return tdir; } auto tdir = (fs::path(dir) / "default").string(); if (Utils::existsAndIsDir(tdir)) return tdir; if (allowRoot && Utils::existsAndIsDir(dir)) return dir; return {}; } static std::string readFileToString(const std::string &filename) { std::ifstream file(filename); if (!file.is_open()) throw std::runtime_error(std::format("Could not open file: {}", filename)); std::stringstream buffer; buffer << file.rdbuf(); return buffer.str(); } void Config::loadFromFile( const std::string &fname, const std::string &enforcedWorkspaceDir, const std::string &enforcedExportDir) { // read the configuration JSON file auto jsonData = readFileToString(fname); auto doc = Yaml::parseDocument(jsonData); auto root = fy_document_root(doc.get()); if (!root || fy_node_get_type(root) != FYNT_MAPPING) { throw std::runtime_error("Invalid JSON configuration file"); } auto workspaceDirNode = Yaml::nodeByKey(root, "WorkspaceDir"); if (workspaceDirNode) { m_workspaceDir = fs::path(Yaml::nodeStrValue(workspaceDirNode)); } else { m_workspaceDir = fs::path(fname).parent_path(); if (m_workspaceDir.empty()) m_workspaceDir = fs::current_path(); } // allow overriding the workspace location if (!enforcedWorkspaceDir.empty()) m_workspaceDir = enforcedWorkspaceDir; if (!fs::path(m_workspaceDir).is_absolute()) m_workspaceDir = fs::absolute(m_workspaceDir); auto projectNameNode = Yaml::nodeByKey(root, "ProjectName"); projectName = projectNameNode ? Yaml::nodeStrValue(projectNameNode) : "Unknown"; auto archiveRootNode = Yaml::nodeByKey(root, "ArchiveRoot"); if (!archiveRootNode) { throw std::runtime_error("ArchiveRoot is required in configuration"); } archiveRoot = Yaml::nodeStrValue(archiveRootNode); auto mediaBaseUrlNode = Yaml::nodeByKey(root, "MediaBaseUrl"); mediaBaseUrl = mediaBaseUrlNode ? Yaml::nodeStrValue(mediaBaseUrlNode) : ""; auto htmlBaseUrlNode = Yaml::nodeByKey(root, "HtmlBaseUrl"); htmlBaseUrl = htmlBaseUrlNode ? Yaml::nodeStrValue(htmlBaseUrlNode) : ""; // set root export directory if (enforcedExportDir.empty()) { m_exportDir = fs::path(m_workspaceDir) / "export"; } else { m_exportDir = enforcedExportDir; logInfo("Using data export directory root from the command-line: {}", m_exportDir.string()); } if (!m_exportDir.is_absolute()) m_exportDir = fs::absolute(m_exportDir); // set the default export directory locations, allow people to override them in the config // (we convert the relative to absolute paths later) mediaExportDir = "media"; dataExportDir = "data"; hintsExportDir = "hints"; htmlExportDir = "html"; auto exportDirsNode = Yaml::nodeByKey(root, "ExportDirs"); if (exportDirsNode && fy_node_get_type(exportDirsNode) == FYNT_MAPPING) { fy_node_pair *pair; void *iter = nullptr; while ((pair = fy_node_mapping_iterate(exportDirsNode, &iter)) != nullptr) { auto keyNode = fy_node_pair_key(pair); auto valueNode = fy_node_pair_value(pair); auto key = Yaml::nodeStrValue(keyNode); auto value = Yaml::nodeStrValue(valueNode); if (key == "Media") { mediaExportDir = value; } else if (key == "Data") { dataExportDir = value; } else if (key == "Hints") { hintsExportDir = value; } else if (key == "Html") { htmlExportDir = value; } else { logWarning("Unknown export directory specifier in config: {}", key); } } } // convert export directory paths to absolute paths if necessary auto makeAbsoluteExportPath = [&](const fs::path &path) { return path.is_absolute() ? path : fs::absolute(fs::path(m_exportDir) / path); }; mediaExportDir = makeAbsoluteExportPath(mediaExportDir); dataExportDir = makeAbsoluteExportPath(dataExportDir); hintsExportDir = makeAbsoluteExportPath(hintsExportDir); htmlExportDir = makeAbsoluteExportPath(htmlExportDir); // a place where external metainfo data can be injected auto extraMetainfoDir = m_workspaceDir / "extra-metainfo"; auto extraMetainfoDirNode = Yaml::nodeByKey(root, "ExtraMetainfoDir"); if (extraMetainfoDirNode) extraMetainfoDir = Yaml::nodeStrValue(extraMetainfoDirNode); auto caInfoNode = Yaml::nodeByKey(root, "CAInfo"); if (caInfoNode) caInfo = Yaml::nodeStrValue(caInfoNode); // allow specifying the AppStream format version we build data for. auto formatVersionNode = Yaml::nodeByKey(root, "FormatVersion"); if (formatVersionNode) { auto versionStr = Yaml::nodeStrValue(formatVersionNode); if (versionStr == "1.0") { formatVersion = AS_FORMAT_VERSION_V1_0; } else { logWarning( "Configuration tried to set unknown AppStream format version '{}'. Falling back to default version.", versionStr); } } // we default to the Debian backend for now metadataType = DataType::XML; std::string backendId = "debian"; auto backendNode = Yaml::nodeByKey(root, "Backend"); if (backendNode) backendId = Utils::toLower(Yaml::nodeStrValue(backendNode)); if (backendId == "dummy") { backendName = "Dummy"; backend = Backend::Dummy; metadataType = DataType::YAML; } else if (backendId == "debian") { backendName = "Debian"; backend = Backend::Debian; metadataType = DataType::YAML; } else if (backendId == "ubuntu") { backendName = "Ubuntu"; backend = Backend::Ubuntu; metadataType = DataType::YAML; } else if (backendId == "arch" || backendId == "archlinux") { backendName = "Arch Linux"; backend = Backend::Archlinux; metadataType = DataType::XML; } else if (backendId == "mageia" || backendId == "rpmmd") { backendName = "RpmMd"; backend = Backend::RpmMd; metadataType = DataType::XML; } else if (backendId == "alpinelinux") { backendName = "Alpine Linux"; backend = Backend::Alpinelinux; metadataType = DataType::XML; } else if (backendId == "freebsd") { backendName = "FreeBSD"; backend = Backend::FreeBSD; metadataType = DataType::XML; } // override the backend's default metadata type if requested by user auto metadataTypeNode = Yaml::nodeByKey(root, "MetadataType"); if (metadataTypeNode) { auto mdataTypeStr = Utils::toLower(Yaml::nodeStrValue(metadataTypeNode)); if (mdataTypeStr == "yaml") { metadataType = DataType::YAML; } else if (mdataTypeStr == "xml") { metadataType = DataType::XML; } else { logError("Invalid value '{}' for MetadataType setting.", mdataTypeStr); } } // suite selections bool hasImmutableSuites = false; auto suitesNode = Yaml::nodeByKey(root, "Suites"); if (suitesNode && fy_node_get_type(suitesNode) == FYNT_MAPPING) { fy_node_pair *pair; void *iter = nullptr; while ((pair = fy_node_mapping_iterate(suitesNode, &iter)) != nullptr) { auto keyNode = fy_node_pair_key(pair); auto valueNode = fy_node_pair_value(pair); auto suiteName = Yaml::nodeStrValue(keyNode); Suite suite; suite.name = suiteName; // Having a suite named "pool" will result in the media pool being copied on // itself if immutableSuites is used. Since 'pool' is a bad suite name anyway, // we error out early on this. if (suiteName == "pool") throw std::runtime_error("The name 'pool' is forbidden for a suite."); auto dataPriorityNode = Yaml::nodeByKey(valueNode, "dataPriority"); if (dataPriorityNode) suite.dataPriority = static_cast(Yaml::nodeIntValue(dataPriorityNode)); auto baseSuiteNode = Yaml::nodeByKey(valueNode, "baseSuite"); if (baseSuiteNode) suite.baseSuite = Yaml::nodeStrValue(baseSuiteNode); auto iconThemeNode = Yaml::nodeByKey(valueNode, "useIconTheme"); if (iconThemeNode) suite.iconTheme = Yaml::nodeStrValue(iconThemeNode); auto sectionsNode = Yaml::nodeByKey(valueNode, "sections"); if (sectionsNode) suite.sections = Yaml::nodeArrayValues(sectionsNode); auto architecturesNode = Yaml::nodeByKey(valueNode, "architectures"); if (architecturesNode) suite.architectures = Yaml::nodeArrayValues(architecturesNode); auto immutableNode = Yaml::nodeByKey(valueNode, "immutable"); if (immutableNode) { suite.isImmutable = Yaml::nodeBoolValue(immutableNode); if (suite.isImmutable) { hasImmutableSuites = true; } } auto suiteExtraMIDir = extraMetainfoDir / suite.name; if (fs::exists(suiteExtraMIDir) && fs::is_directory(suiteExtraMIDir)) suite.extraMetainfoDir = std::move(suiteExtraMIDir); suites.push_back(std::move(suite)); } } auto oldsuitesNode = Yaml::nodeByKey(root, "Oldsuites"); if (oldsuitesNode) oldsuites = Yaml::nodeArrayValues(oldsuitesNode); // icon policy auto iconsNode = Yaml::nodeByKey(root, "Icons"); if (iconsNode && fy_node_get_type(iconsNode) == FYNT_MAPPING) { fy_node_pair *pair; void *iter = nullptr; while ((pair = fy_node_mapping_iterate(iconsNode, &iter)) != nullptr) { auto keyNode = fy_node_pair_key(pair); auto valueNode = fy_node_pair_value(pair); auto iconString = Yaml::nodeStrValue(keyNode); // Parse icon size in ImageSize constructor ImageSize iconSize; bool isBadIconSize = false; try { iconSize = ImageSize(iconString); if (iconSize.width == 0) isBadIconSize = true; } catch (const std::exception &e) { isBadIconSize = true; } if (isBadIconSize) { logError("Malformed icon size '{}' found in configuration, icon policy has been ignored.", iconString); continue; } // Check if the parsed icon size is in the list of allowed icon sizes bool isAllowed = false; for (const auto &allowedSize : AllowedIconSizes) { if (allowedSize == iconSize) { isAllowed = true; break; } } if (!isAllowed) { logError("Invalid icon size '{}' selected in configuration, icon policy has been ignored.", iconString); continue; } bool storeRemote = false; bool storeCached = false; auto remoteNode = Yaml::nodeByKey(valueNode, "remote"); if (remoteNode) storeRemote = Yaml::nodeBoolValue(remoteNode); auto cachedNode = Yaml::nodeByKey(valueNode, "cached"); if (cachedNode) storeCached = Yaml::nodeBoolValue(cachedNode); AscIconState istate = ASC_ICON_STATE_IGNORED; if (storeRemote && storeCached) { istate = ASC_ICON_STATE_CACHED_REMOTE; } else if (storeRemote) { istate = ASC_ICON_STATE_REMOTE_ONLY; } else if (storeCached) { istate = ASC_ICON_STATE_CACHED_ONLY; } // sanity check if (iconSize == ImageSize(64)) { if (!storeCached) { logError( "The icon size 64x64 must always be present and be allowed to be cached. Ignored user " "configuration."); continue; } } // set new policy, overriding existing one asc_icon_policy_set_policy(m_iconPolicy, iconSize.width, iconSize.scale, istate); } } maxScrFileSize = 14; // 14MiB is the default maximum size auto maxScrFileSizeNode = Yaml::nodeByKey(root, "MaxScreenshotFileSize"); if (maxScrFileSizeNode) maxScrFileSize = Yaml::nodeIntValue(maxScrFileSizeNode); auto allowedCustomKeysNode = Yaml::nodeByKey(root, "AllowedCustomKeys"); if (allowedCustomKeysNode) { auto keysList = Yaml::nodeArrayValues(allowedCustomKeysNode); for (const auto &key : keysList) allowedCustomKeys[key] = true; } // Enable features which are default-enabled feature.processDesktop = true; feature.validate = true; feature.storeScreenshots = true; feature.optipng = true; feature.metadataTimestamps = true; feature.immutableSuites = true; feature.processFonts = true; feature.allowIconUpscale = true; feature.processGStreamer = true; feature.processLocale = true; feature.screenshotVideos = true; // apply vendor feature settings auto featuresNode = Yaml::nodeByKey(root, "Features"); if (featuresNode && fy_node_get_type(featuresNode) == FYNT_MAPPING) { fy_node_pair *pair; void *iter = nullptr; while ((pair = fy_node_mapping_iterate(featuresNode, &iter)) != nullptr) { auto keyNode = fy_node_pair_key(pair); auto valueNode = fy_node_pair_value(pair); auto featureId = Yaml::nodeStrValue(keyNode); auto featureValue = Yaml::nodeBoolValue(valueNode); if (featureId == "validateMetainfo") { feature.validate = featureValue; } else if (featureId == "processDesktop") { feature.processDesktop = featureValue; } else if (featureId == "noDownloads") { feature.noDownloads = featureValue; } else if (featureId == "createScreenshotsStore") { feature.storeScreenshots = featureValue; } else if (featureId == "optimizePNGSize") { feature.optipng = featureValue; } else if (featureId == "metadataTimestamps") { feature.metadataTimestamps = featureValue; } else if (featureId == "immutableSuites") { feature.immutableSuites = featureValue; } else if (featureId == "processFonts") { feature.processFonts = featureValue; } else if (featureId == "allowIconUpscaling") { feature.allowIconUpscale = featureValue; } else if (featureId == "processGStreamer") { feature.processGStreamer = featureValue; } else if (featureId == "processLocale") { feature.processLocale = featureValue; } else if (featureId == "screenshotVideos") { feature.screenshotVideos = featureValue; } else if (featureId == "propagateMetaInfoArtifacts") { feature.propagateMetaInfoArtifacts = featureValue; } } } // check if we need to disable features because some prerequisites are not met if (feature.optipng) { if (optipngBinary.empty()) { feature.optipng = false; logError("Disabled feature `optimizePNGSize`: The `optipng` binary was not found."); } else { logDebug("Using `optipng`: {}", optipngBinary); } } asc_globals_set_use_optipng(feature.optipng); if (feature.screenshotVideos) { if (ffprobeBinary.empty()) { feature.screenshotVideos = false; logError("Disabled feature `screenshotVideos`: The `ffprobe` binary was not found."); } else { logDebug("Using `ffprobe`: {}", ffprobeBinary); } } if (feature.noDownloads) { // since disallowing network access might have quite a lot of sideeffects, we print // a message to the logs to make debugging easier. // in general, running with noDownloads is discouraged. logWarning("Configuration does not permit downloading files. Several features will not be available."); } if (!feature.immutableSuites) { // Immutable suites won't work if the feature is disabled - log this error if (hasImmutableSuites) { logError( "Suites are defined as immutable, but the `immutableSuites` feature is disabled. Immutability will not " "work!"); } } if (!feature.validate) logWarning("MetaInfo validation has been disabled in configuration."); // sanity check to warn if our GdkPixbuf does not support the minimum amount // of image formats we need g_autoptr(GHashTable) pbFormatNames = asc_image_supported_format_names(); if (!g_hash_table_contains(pbFormatNames, "png") || !g_hash_table_contains(pbFormatNames, "svg") || !g_hash_table_contains(pbFormatNames, "jpeg")) { logError( "The currently used GdkPixbuf does not seem to support all image formats we require to run normally " "(png/svg/jpeg). This may be a problem with your installation of appstream-generator or gdk-pixbuf."); } if (!g_hash_table_contains(pbFormatNames, "jxl")) logWarning("JPEG-XL image support not found!"); } bool Config::isValid() const { return !projectName.empty(); } /** * Get unique temporary directory to use during one generator run. */ fs::path Config::getTmpDir() const { static std::mutex tmpDirMutex; std::lock_guard lock(tmpDirMutex); if (m_tmpDir.empty()) { std::string root; if (cacheRootDir().empty()) root = "/tmp/"; else root = cacheRootDir(); m_tmpDir = fs::path(root) / "tmp" / std::format("asgen-{}", Utils::randomString(8)); // make appstream-compose internal functions aware of the new temp dir asc_globals_set_tmp_dir(m_tmpDir.c_str()); } return m_tmpDir; } void Config::setWorkspaceDir(const fs::path &dir) { m_workspaceDir = dir; } } // namespace ASGenerator appstream-generator-0.10.1/src/config.h000066400000000000000000000107141506754475600200040ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include #include "utils.h" typedef struct _AscIconPolicy AscIconPolicy; namespace ASGenerator { /** * A list of valid icon sizes that we recognize in AppStream. */ inline constexpr std::array AllowedIconSizes = {ImageSize(48), ImageSize(48, 48, 2), ImageSize(64), ImageSize(64, 64, 2), ImageSize(128), ImageSize(128, 128, 2)}; /** * Fake package name AppStream Generator uses internally to inject additional metainfo on users' request */ inline constexpr std::string EXTRA_METAINFO_FAKE_PKGNAME = "+extra-metainfo"; /** * Describes a suite in a software repository. */ struct Suite { std::string name; int dataPriority = 0; std::string baseSuite; std::string iconTheme; std::vector sections; std::vector architectures; fs::path extraMetainfoDir; bool isImmutable = false; }; /** * The AppStream metadata type we want to generate. */ enum class DataType { XML, YAML }; /** * Distribution-specific backends. */ enum class Backend { Unknown, Dummy, Debian, Ubuntu, Archlinux, RpmMd, Alpinelinux, FreeBSD }; /** * Generator features that can be toggled by the user. */ struct GeneratorFeatures { bool processDesktop = true; bool validate = true; bool noDownloads = false; bool storeScreenshots = true; bool optipng = true; bool metadataTimestamps = true; bool immutableSuites = true; bool processFonts = true; bool allowIconUpscale = true; bool processGStreamer = true; bool processLocale = true; bool screenshotVideos = true; bool propagateMetaInfoArtifacts = false; }; /// Fake package name AppStream Generator uses internally to inject additional metainfo on users' request extern const std::string EXTRA_METAINFO_FAKE_PKGNAME; /** * The global configuration for the metadata generator. */ class Config { public: ~Config(); // Singleton access static Config &get(); // Configuration properties AsFormatVersion formatVersion; std::string projectName; std::string archiveRoot; std::string mediaBaseUrl; std::string htmlBaseUrl; std::string backendName; Backend backend; std::vector suites; std::vector oldsuites; DataType metadataType; GeneratorFeatures feature; std::string optipngBinary; std::string ffprobeBinary; std::unordered_map allowedCustomKeys; fs::path dataExportDir; fs::path hintsExportDir; fs::path mediaExportDir; fs::path htmlExportDir; int64_t maxScrFileSize; std::string caInfo; std::string formatVersionStr() const; fs::path databaseDir() const; fs::path cacheRootDir() const; fs::path templateDir() const; AscIconPolicy *iconPolicy() const; void loadFromFile( const std::string &fname, const std::string &enforcedWorkspaceDir = "", const std::string &enforcedExportDir = ""); bool isValid() const; fs::path getTmpDir() const; void setWorkspaceDir(const fs::path &dir); // Delete copy constructor and assignment operator for singleton Config(const Config &) = delete; Config &operator=(const Config &) = delete; private: static std::unique_ptr instance_; static std::once_flag initialized_; Config(); fs::path m_workspaceDir; fs::path m_exportDir; mutable fs::path m_tmpDir; AscIconPolicy *m_iconPolicy; fs::path getVendorTemplateDir(const std::string &dir, bool allowRoot = false) const; }; } // namespace ASGenerator appstream-generator-0.10.1/src/contentsstore.cpp000066400000000000000000000325361506754475600220120ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "contentsstore.h" #include #include #include #include #include #include #include #include "config.h" #include "logging.h" namespace fs = std::filesystem; namespace ASGenerator { ContentsStore::ContentsStore() : dbEnv(nullptr), m_opened(false) { } ContentsStore::~ContentsStore() { close(); } void ContentsStore::checkError(int rc, const std::string &msg) { if (rc != 0) throw std::runtime_error(std::format("{}[{}]: {}", msg, rc, mdb_strerror(rc))); } void ContentsStore::open(const std::string &dir) { int rc; if (m_opened) throw std::runtime_error("ContentsStore was already opened."); logDebug("Opening contents cache."); // ensure the cache directory exists fs::create_directories(dir); rc = mdb_env_create(&dbEnv); if (rc != 0) { checkError(rc, "mdb_env_create"); return; } // We are going to use at max 3 sub-databases: // contents, icons and locale rc = mdb_env_set_maxdbs(dbEnv, 3); if (rc != 0) { mdb_env_close(dbEnv); checkError(rc, "mdb_env_set_maxdbs"); return; } // set a huge map size to be futureproof. // This means we're cruel to non-64bit users, but this // software is supposed to be run on 64bit machines anyway. auto mapsize = static_cast(std::pow(512L, 4)); rc = mdb_env_set_mapsize(dbEnv, mapsize); if (rc != 0) { mdb_env_close(dbEnv); checkError(rc, "mdb_env_set_mapsize"); return; } // open database rc = mdb_env_open(dbEnv, dir.c_str(), MDB_NOMETASYNC, 0755); if (rc != 0) { mdb_env_close(dbEnv); checkError(rc, "mdb_env_open"); return; } // open sub-databases in the environment MDB_txn *txn; rc = mdb_txn_begin(dbEnv, nullptr, 0, &txn); if (rc != 0) { mdb_env_close(dbEnv); checkError(rc, "mdb_txn_begin"); return; } try { // contains a full list of all contents rc = mdb_dbi_open(txn, "contents", MDB_CREATE, &dbContents); checkError(rc, "open contents database"); // contains list of icon files and related data // the contents sub-database exists only to allow building instances // of IconHandler much faster. rc = mdb_dbi_open(txn, "icondata", MDB_CREATE, &dbIcons); checkError(rc, "open icon-info database"); // contains list of locale files and related data rc = mdb_dbi_open(txn, "localedata", MDB_CREATE, &dbLocale); checkError(rc, "open locale-info database"); rc = mdb_txn_commit(txn); checkError(rc, "mdb_txn_commit"); m_opened = true; } catch (...) { mdb_txn_abort(txn); mdb_env_close(dbEnv); throw; } } void ContentsStore::open(const Config &conf) { auto path = conf.databaseDir() / "contents"; open(path.string()); } void ContentsStore::close() { std::lock_guard lock(m_mutex); if (m_opened && dbEnv) { mdb_env_close(dbEnv); m_opened = false; dbEnv = nullptr; } } MDB_val ContentsStore::makeDbValue(const std::string &data) { MDB_val mval; mval.mv_size = data.length() + 1; mval.mv_data = const_cast(static_cast(data.c_str())); return mval; } MDB_txn *ContentsStore::newTransaction(unsigned int flags) { assert(m_opened); int rc; MDB_txn *txn; rc = mdb_txn_begin(dbEnv, nullptr, flags, &txn); checkError(rc, "mdb_txn_begin"); return txn; } void ContentsStore::commitTransaction(MDB_txn *txn) { auto rc = mdb_txn_commit(txn); checkError(rc, "mdb_txn_commit"); } void ContentsStore::quitTransaction(MDB_txn *txn) { if (txn == nullptr) return; mdb_txn_abort(txn); } void ContentsStore::removePackage(const std::string &pkid) { MDB_val key = makeDbValue(pkid); auto txn = newTransaction(); try { auto res = mdb_del(txn, dbContents, &key, nullptr); checkError(res, "mdb_del (contents)"); res = mdb_del(txn, dbIcons, &key, nullptr); if (res != MDB_NOTFOUND) checkError(res, "mdb_del (icons)"); res = mdb_del(txn, dbLocale, &key, nullptr); if (res != MDB_NOTFOUND) checkError(res, "mdb_del (locale)"); commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } bool ContentsStore::packageExists(const std::string &pkid) { MDB_val dkey = makeDbValue(pkid); MDB_cursor *cur = nullptr; auto txn = newTransaction(MDB_RDONLY); try { auto res = mdb_cursor_open(txn, dbContents, &cur); checkError(res, "mdb_cursor_open"); res = mdb_cursor_get(cur, &dkey, nullptr, MDB_SET); mdb_cursor_close(cur); cur = nullptr; if (res == MDB_NOTFOUND) { quitTransaction(txn); return false; } checkError(res, "mdb_cursor_get"); quitTransaction(txn); return true; } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } void ContentsStore::addContents(const std::string &pkid, const std::vector &contents) { // filter out icon filenames and filenames of icon-related stuff (e.g. theme.index), // as well as locale information std::vector iconInfo; std::vector localeInfo; for (const auto &f : contents) { if (f.starts_with("/usr/share/icons/") || f.starts_with("/usr/share/pixmaps/")) { iconInfo.push_back(f); continue; } // create a huge index of all Gettext and Qt translation filenames if (f.ends_with(".mo") || f.ends_with(".qm")) { localeInfo.push_back(f); continue; } } // Join contents with newlines std::ostringstream contentsStream; for (size_t i = 0; i < contents.size(); ++i) { if (i > 0) contentsStream << "\n"; contentsStream << contents[i]; } const std::string contentsStr = contentsStream.str(); std::lock_guard lock(m_mutex); auto key = makeDbValue(pkid); auto contentsVal = makeDbValue(contentsStr); auto txn = newTransaction(); try { auto res = mdb_put(txn, dbContents, &key, &contentsVal, 0); checkError(res, "mdb_put"); // if we have icon information, store that too if (!iconInfo.empty()) { std::ostringstream iconsStream; for (size_t i = 0; i < iconInfo.size(); ++i) { if (i > 0) iconsStream << "\n"; iconsStream << iconInfo[i]; } const std::string iconsStr = iconsStream.str(); MDB_val iconsVal = makeDbValue(iconsStr); res = mdb_put(txn, dbIcons, &key, &iconsVal, 0); checkError(res, "mdb_put (icons)"); } // store locale if (!localeInfo.empty()) { std::ostringstream localeStream; for (size_t i = 0; i < localeInfo.size(); ++i) { if (i > 0) localeStream << "\n"; localeStream << localeInfo[i]; } const std::string localeStr = localeStream.str(); MDB_val localeVal = makeDbValue(localeStr); res = mdb_put(txn, dbLocale, &key, &localeVal, 0); checkError(res, "mdb_put (locale)"); } commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } std::unordered_map ContentsStore::getFilesMap( const std::vector &pkids, MDB_dbi dbi, bool useBaseName) { MDB_cursor *cur; auto txn = newTransaction(MDB_RDONLY); std::unordered_map pkgCMap; try { auto res = mdb_cursor_open(txn, dbi, &cur); checkError(res, "mdb_cursor_open"); for (const auto &pkid : pkids) { MDB_val pkey = makeDbValue(pkid); MDB_val cval; res = mdb_cursor_get(cur, &pkey, &cval, MDB_SET); if (res == MDB_NOTFOUND) continue; checkError(res, "mdb_cursor_get"); auto data = static_cast(cval.mv_data); std::string contents(data); std::istringstream stream(contents); std::string line; while (std::getline(stream, line)) { if (useBaseName) { auto pos = line.find_last_of('/'); std::string basename = (pos != std::string::npos) ? line.substr(pos + 1) : line; pkgCMap[basename] = pkid; } else { pkgCMap[line] = pkid; } } } mdb_cursor_close(cur); quitTransaction(txn); } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } return pkgCMap; } std::unordered_map ContentsStore::getContentsMap(const std::vector &pkids) { return getFilesMap(pkids, dbContents); } std::unordered_map ContentsStore::getIconFilesMap(const std::vector &pkids) { return getFilesMap(pkids, dbIcons); } std::unordered_map ContentsStore::getLocaleMap(const std::vector &pkids) { // we make the assumption here that all locale for a given domain are in one package. // otherwise this global search will get even more insane. return getFilesMap(pkids, dbLocale); } std::vector ContentsStore::getContentsList(const std::string &pkid, MDB_dbi dbi) { MDB_val pkey = makeDbValue(pkid); MDB_val cval; MDB_cursor *cur; auto txn = newTransaction(MDB_RDONLY); std::vector result; try { auto res = mdb_cursor_open(txn, dbi, &cur); checkError(res, "mdb_cursor_open"); res = mdb_cursor_get(cur, &pkey, &cval, MDB_SET); if (res == MDB_NOTFOUND) { mdb_cursor_close(cur); quitTransaction(txn); return result; } checkError(res, "mdb_cursor_get"); auto data = static_cast(cval.mv_data); std::string contentsStr(data); std::istringstream stream(contentsStr); std::string line; while (std::getline(stream, line)) result.push_back(line); mdb_cursor_close(cur); quitTransaction(txn); } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } return result; } std::vector ContentsStore::getContents(const std::string &pkid) { return getContentsList(pkid, dbContents); } std::vector ContentsStore::getIcons(const std::string &pkid) { return getContentsList(pkid, dbIcons); } std::vector ContentsStore::getLocaleFiles(const std::string &pkid) { return getContentsList(pkid, dbLocale); } std::unordered_set ContentsStore::getPackageIdSet() { MDB_cursor *cur; auto txn = newTransaction(); std::unordered_set pkgSet; try { auto res = mdb_cursor_open(txn, dbContents, &cur); checkError(res, "mdb_cursor_open (getPackageIdSet)"); MDB_val pkey; while (mdb_cursor_get(cur, &pkey, nullptr, MDB_NEXT) == 0) { auto data = static_cast(pkey.mv_data); std::string pkid(data); pkgSet.insert(std::move(pkid)); } mdb_cursor_close(cur); quitTransaction(txn); } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } return pkgSet; } void ContentsStore::removePackages(const std::unordered_set &pkidSet) { auto txn = newTransaction(); try { for (const auto &pkid : pkidSet) { auto key = makeDbValue(pkid); auto res = mdb_del(txn, dbContents, &key, nullptr); checkError(res, "mdb_del (contents)"); res = mdb_del(txn, dbIcons, &key, nullptr); if (res != MDB_NOTFOUND) checkError(res, "mdb_del (icons)"); res = mdb_del(txn, dbLocale, &key, nullptr); if (res != MDB_NOTFOUND) checkError(res, "mdb_del (locale)"); } commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } void ContentsStore::sync() { assert(m_opened); mdb_env_sync(dbEnv, 1); } } // namespace ASGenerator appstream-generator-0.10.1/src/contentsstore.h000066400000000000000000000062751506754475600214600ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include namespace ASGenerator { class Config; /** * Contains a cache about available files in packages. * This is useful for finding icons and for quickly * re-scanning packages which may become interesting later. */ class ContentsStore { public: ContentsStore(); ~ContentsStore(); void open(const std::string &dir); void open(const Config &conf); void close(); /** * Drop a package-id from the contents cache. */ void removePackage(const std::string &pkid); bool packageExists(const std::string &pkid); void addContents(const std::string &pkid, const std::vector &contents); std::unordered_map getContentsMap(const std::vector &pkids); std::unordered_map getIconFilesMap(const std::vector &pkids); /** * We make the assumption here that all locale for a given domain are in one package. * Otherwise this global search will get even more insane. */ std::unordered_map getLocaleMap(const std::vector &pkids); std::vector getContents(const std::string &pkid); std::vector getIcons(const std::string &pkid); std::vector getLocaleFiles(const std::string &pkid); std::unordered_set getPackageIdSet(); void removePackages(const std::unordered_set &pkidSet); void sync(); // Delete copy constructor and assignment operator ContentsStore(const ContentsStore &) = delete; ContentsStore &operator=(const ContentsStore &) = delete; private: MDB_env *dbEnv; MDB_dbi dbContents{0}; MDB_dbi dbIcons{0}; MDB_dbi dbLocale{0}; bool m_opened; std::mutex m_mutex; void checkError(int rc, const std::string &msg); MDB_val makeDbValue(const std::string &data); MDB_txn *newTransaction(unsigned int flags = 0); void commitTransaction(MDB_txn *txn); void quitTransaction(MDB_txn *txn); std::unordered_map getFilesMap( const std::vector &pkids, MDB_dbi dbi, bool useBaseName = false); std::vector getContentsList(const std::string &pkid, MDB_dbi dbi); }; } // namespace ASGenerator appstream-generator-0.10.1/src/cptmodifiers.cpp000066400000000000000000000141761506754475600215700ustar00rootroot00000000000000/* * Copyright (C) 2021-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "cptmodifiers.h" #include #include #include #include #include "logging.h" #include "result.h" #include "utils.h" #include "yaml-utils.h" namespace ASGenerator { InjectedModifications::InjectedModifications() : m_hasRemovedCpts(false), m_hasInjectedCustom(false) { } InjectedModifications::~InjectedModifications() { for (auto &[key, cpt] : m_removedComponents) g_object_unref(cpt); } void InjectedModifications::loadForSuite(std::shared_ptr suite) { std::unique_lock lock(m_mutex); // Clear existing data and unreference components for (auto &[key, component] : m_removedComponents) g_object_unref(component); m_removedComponents.clear(); m_injectedCustomData.clear(); const auto fname = suite->extraMetainfoDir / "modifications.json"; if (!fs::exists(fname)) return; logInfo("Using repo-level modifications for {} (via modifications.json)", suite->name); // Read the JSON file std::ifstream file(fname); if (!file.is_open()) throw std::runtime_error(std::format("Failed to open modifications file: {}", fname.string())); std::string jsonData((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); // Parse JSON auto doc = Yaml::parseDocument(jsonData, true); auto root = Yaml::documentRoot(doc); if (!root || fy_node_get_type(root) != FYNT_MAPPING) throw std::runtime_error(std::format("Failed to parse modifications JSON file: {}", fname.string())); // Process InjectCustom section auto injectCustomNode = fy_node_mapping_lookup_by_string(root, "InjectCustom", FY_NT); if (injectCustomNode && fy_node_get_type(injectCustomNode) == FYNT_MAPPING) { logDebug("Using injected custom entries from {}", fname.string()); fy_node_pair *pair; void *iter = nullptr; while ((pair = fy_node_mapping_iterate(injectCustomNode, &iter)) != nullptr) { fy_node *keyNode = fy_node_pair_key(pair); fy_node *valueNode = fy_node_pair_value(pair); if (!keyNode || !valueNode) continue; size_t keyLen = 0; const char *keyStr = fy_node_get_scalar(keyNode, &keyLen); if (!keyStr) continue; std::string entryKey(keyStr, keyLen); if (fy_node_get_type(valueNode) == FYNT_MAPPING) { std::unordered_map customData; fy_node_pair *customPair; void *customIter = nullptr; while ((customPair = fy_node_mapping_iterate(valueNode, &customIter)) != nullptr) { fy_node *customKeyNode = fy_node_pair_key(customPair); fy_node *customValueNode = fy_node_pair_value(customPair); if (!customKeyNode || !customValueNode) continue; auto customKey = Yaml::nodeStrValue(customKeyNode); auto customValue = Yaml::nodeStrValue(customValueNode); if (!customKey.empty() && !customValue.empty()) customData[customKey] = std::move(customValue); } m_injectedCustomData[entryKey] = std::move(customData); } } } // Process Remove section fy_node *removeNode = fy_node_mapping_lookup_by_string(root, "Remove", FY_NT); if (removeNode && fy_node_get_type(removeNode) == FYNT_SEQUENCE) { logDebug("Using package removal info from {}", fname.string()); fy_node *cidNode; void *iter = nullptr; while ((cidNode = fy_node_sequence_iterate(removeNode, &iter)) != nullptr) { auto cid = Yaml::nodeStrValue(cidNode); if (cid.empty()) continue; g_autoptr(AsComponent) cpt = as_component_new(); as_component_set_kind(cpt, AS_COMPONENT_KIND_GENERIC); as_component_set_merge_kind(cpt, AS_MERGE_KIND_REMOVE_COMPONENT); as_component_set_id(cpt, cid.c_str()); m_removedComponents[cid] = g_steal_pointer(&cpt); } } m_hasRemovedCpts = !m_removedComponents.empty(); m_hasInjectedCustom = !m_injectedCustomData.empty(); } bool InjectedModifications::hasRemovedComponents() const { return m_hasRemovedCpts; } /** * Test if component was marked for deletion. */ bool InjectedModifications::isComponentRemoved(const std::string &cid) const { if (!m_hasRemovedCpts) return false; std::shared_lock lock(m_mutex); return m_removedComponents.contains(cid); } std::optional> InjectedModifications::injectedCustomData( const std::string &cid) const { std::shared_lock lock(m_mutex); if (!m_hasInjectedCustom) return std::nullopt; auto it = m_injectedCustomData.find(cid); if (it == m_injectedCustomData.end()) return std::nullopt; return it->second; } void InjectedModifications::addRemovalRequestsToResult(GeneratorResult *gres) const { std::shared_lock lock(m_mutex); for (const auto &[cid, cpt] : m_removedComponents) { gres->addComponentWithString(cpt, std::format("{}/-{}", gres->pkid(), cid)); } } } // namespace ASGenerator appstream-generator-0.10.1/src/cptmodifiers.h000066400000000000000000000042641506754475600212320ustar00rootroot00000000000000/* * Copyright (C) 2021-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include "config.h" namespace ASGenerator { class GeneratorResult; /** * Helper class to provide information about repository-specific metadata modifications. * Instances of this class must be thread safe. */ class InjectedModifications { public: InjectedModifications(); ~InjectedModifications(); void loadForSuite(std::shared_ptr suite); bool hasRemovedComponents() const; /** * Test if component was marked for deletion. */ bool isComponentRemoved(const std::string &cid) const; /** * Get injected custom data entries. */ std::optional> injectedCustomData(const std::string &cid) const; void addRemovalRequestsToResult(GeneratorResult *gres) const; // Delete copy constructor and assignment operator InjectedModifications(const InjectedModifications &) = delete; InjectedModifications &operator=(const InjectedModifications &) = delete; private: std::unordered_map m_removedComponents; std::unordered_map> m_injectedCustomData; bool m_hasRemovedCpts; bool m_hasInjectedCustom; mutable std::shared_mutex m_mutex; }; } // namespace ASGenerator appstream-generator-0.10.1/src/datainjectpkg.cpp000066400000000000000000000146661506754475600217140ustar00rootroot00000000000000/* * Copyright (C) 2018-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "datainjectpkg.h" #include #include #include #include "logging.h" #include "utils.h" namespace ASGenerator { DataInjectPackage::DataInjectPackage(const std::string &pname, const std::string &parch) : m_pkgname(pname), m_pkgarch(parch) { } std::string DataInjectPackage::name() const { return m_pkgname; } std::string DataInjectPackage::ver() const { return "0~0"; } std::string DataInjectPackage::arch() const { return m_pkgarch; } PackageKind DataInjectPackage::kind() const noexcept { return PackageKind::Fake; } const std::unordered_map &DataInjectPackage::description() const { return m_desc; } std::string DataInjectPackage::getFilename() { return "_local_"; } std::string DataInjectPackage::maintainer() const { return m_pkgmaintainer; } void DataInjectPackage::setMaintainer(const std::string &maint) { m_pkgmaintainer = maint; } const std::string &DataInjectPackage::dataLocation() const { return m_dataLocation; } void DataInjectPackage::setDataLocation(const std::string &value) { m_dataLocation = value; } const std::string &DataInjectPackage::archDataLocation() const { return m_archDataLocation; } void DataInjectPackage::setArchDataLocation(const std::string &value) { m_archDataLocation = value; } std::vector DataInjectPackage::getFileData(const std::string &fname) { auto it = m_contents.find(fname); if (it == m_contents.end()) return {}; const std::string &localPath = it->second; if (localPath.empty()) return {}; std::vector data; std::ifstream file(localPath, std::ios::binary); if (!file.is_open()) return {}; char buffer[GENERIC_BUFFER_SIZE]; while (file.read(buffer, sizeof(buffer)) || file.gcount() > 0) data.insert(data.end(), buffer, buffer + file.gcount()); return data; } const std::vector &DataInjectPackage::contents() { if (m_contents.empty()) m_contentsVector.clear(); if (!m_contents.empty()) return m_contentsVector; if (m_dataLocation.empty() || !Utils::existsAndIsDir(m_dataLocation)) { m_contentsVector.clear(); return m_contentsVector; } // find all icons const auto iconLocation = fs::path(m_dataLocation) / "icons"; if (Utils::existsAndIsDir(iconLocation)) { try { for (const auto &entry : fs::recursive_directory_iterator(iconLocation)) { if (entry.is_regular_file()) { const auto &iconFname = entry.path(); const auto extension = iconFname.extension(); if (extension == ".svg" || extension == ".svgz" || extension == ".png") { const auto iconBasePath = fs::relative(iconFname, iconLocation); const auto fakePath = fs::path("/usr/share/icons/hicolor") / iconBasePath; m_contents[fakePath.string()] = iconFname.string(); } } } } catch (const fs::filesystem_error &e) { logError("Error scanning icon directory '{}': {}", iconLocation.string(), e.what()); } } else { logInfo("No icons found in '{}' for injected metadata.", iconLocation.string()); } // find metainfo files if (Utils::existsAndIsDir(m_dataLocation)) { try { for (const auto &entry : fs::directory_iterator(m_dataLocation)) { if (entry.is_regular_file()) { const auto &miFname = entry.path(); if (miFname.extension() == ".xml") { const auto miBasename = miFname.filename().string(); logDebug("Found injected metainfo [{}]: {}", "all", miBasename); const auto fakePath = fs::path("/usr/share/metainfo") / miBasename; m_contents[fakePath.string()] = miFname.string(); } } } } catch (const fs::filesystem_error &e) { logError("Error scanning metainfo directory '{}': {}", m_dataLocation, e.what()); } } if (m_archDataLocation.empty() || !Utils::existsAndIsDir(m_archDataLocation)) goto build_vector; // load arch-specific override metainfo files try { for (const auto &entry : fs::directory_iterator(m_archDataLocation)) { if (entry.is_regular_file()) { const auto &miFname = entry.path(); if (miFname.extension() == ".xml") { const auto miBasename = miFname.filename().string(); const auto fakePath = fs::path("/usr/share/metainfo") / miBasename; const auto fakePathStr = fakePath.string(); if (m_contents.find(fakePathStr) != m_contents.end()) { logDebug("Found injected metainfo [{}]: {} (replacing generic one)", arch(), miBasename); } else { logDebug("Found injected metainfo [{}]: {}", arch(), miBasename); } m_contents[fakePathStr] = miFname.string(); } } } } catch (const fs::filesystem_error &e) { logError("Error scanning arch-specific metainfo directory '{}': {}", m_archDataLocation, e.what()); } build_vector: m_contentsVector.clear(); m_contentsVector.reserve(m_contents.size()); for (const auto &[key, value] : m_contents) m_contentsVector.push_back(key); return m_contentsVector; } void DataInjectPackage::finish() { // No-op } } // namespace ASGenerator appstream-generator-0.10.1/src/datainjectpkg.h000066400000000000000000000044631506754475600213530ustar00rootroot00000000000000/* * Copyright (C) 2018-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include "backends/interfaces.h" namespace ASGenerator { /** * Fake package which has the sole purpose of allowing easy injection of local * data that does not reside in packages. */ class DataInjectPackage final : public Package { public: DataInjectPackage(const std::string &pname, const std::string &parch); std::string name() const override; std::string ver() const override; std::string arch() const override; PackageKind kind() const noexcept override; const std::unordered_map &description() const override; std::string getFilename() override; std::string maintainer() const override; void setMaintainer(const std::string &maint); const std::string &dataLocation() const; void setDataLocation(const std::string &value); const std::string &archDataLocation() const; void setArchDataLocation(const std::string &value); const std::vector &contents() override; std::vector getFileData(const std::string &fname) override; void finish() override; private: std::string m_pkgname; std::string m_pkgarch; std::string m_pkgmaintainer; std::unordered_map m_desc; std::unordered_map m_contents; std::string m_dataLocation; std::string m_archDataLocation; mutable std::vector m_contentsVector; }; } // namespace ASGenerator appstream-generator-0.10.1/src/datastore.cpp000066400000000000000000001023061506754475600210570ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "datastore.h" #include #include #include #include #include #include #include #include #include #include "logging.h" #include "result.h" #include "utils.h" namespace ASGenerator { // Helper function for binary serialization of variant maps static std::vector serializeVariantMap( const std::unordered_map> &data, std::optional timestamp = std::nullopt) { std::vector buffer; buffer.reserve(1024); // Version byte for future compatibility buffer.push_back(1); // Serialize timestamp if provided (for StatisticsEntry) if (timestamp) { const auto time_bytes = reinterpret_cast(&*timestamp); buffer.insert(buffer.end(), time_bytes, time_bytes + sizeof(std::size_t)); } // Serialize data map count (4 bytes) const auto data_count = static_cast(data.size()); const auto count_bytes = reinterpret_cast(&data_count); buffer.insert(buffer.end(), count_bytes, count_bytes + sizeof(std::uint32_t)); // Serialize each key-value pair for (const auto &[key, value] : data) { // Key length (2 bytes) + key string const auto key_len = static_cast(key.length()); const auto key_len_bytes = reinterpret_cast(&key_len); buffer.insert(buffer.end(), key_len_bytes, key_len_bytes + sizeof(std::uint16_t)); buffer.insert(buffer.end(), key.begin(), key.end()); // Value type (1 byte) + value data std::visit( [&buffer](const auto &val) { using T = std::decay_t; if constexpr (std::is_same_v) { buffer.push_back(1); // int64 type const auto val_bytes = reinterpret_cast(&val); buffer.insert(buffer.end(), val_bytes, val_bytes + sizeof(std::int64_t)); } else if constexpr (std::is_same_v) { buffer.push_back(2); // double type const auto val_bytes = reinterpret_cast(&val); buffer.insert(buffer.end(), val_bytes, val_bytes + sizeof(double)); } else if constexpr (std::is_same_v) { buffer.push_back(3); // string type const auto str_len = static_cast(val.length()); const auto str_len_bytes = reinterpret_cast(&str_len); buffer.insert(buffer.end(), str_len_bytes, str_len_bytes + sizeof(std::uint16_t)); buffer.insert(buffer.end(), val.begin(), val.end()); } }, value); } return buffer; } // Helper function for binary deserialization of variant maps template T deserializeVariantMap(const std::vector &binary_data, bool has_timestamp = false) { const size_t min_size = has_timestamp ? 13 : 5; // version + [time +] count if (binary_data.size() < min_size) throw std::runtime_error("Invalid data: buffer too small"); size_t pos = 0; T entry{}; // Check version const std::uint8_t version = binary_data[pos++]; if (version != 1) throw std::runtime_error(std::format("Unsupported version: {}", static_cast(version))); // Read timestamp if present (for StatisticsEntry) if constexpr (std::is_same_v) { if (has_timestamp) { std::memcpy(&entry.time, &binary_data[pos], sizeof(std::size_t)); pos += sizeof(std::size_t); } } // Read data count std::uint32_t data_count; std::memcpy(&data_count, &binary_data[pos], sizeof(std::uint32_t)); pos += sizeof(std::uint32_t); // Read key-value pairs for (std::uint32_t i = 0; i < data_count; ++i) { if (pos + 2 > binary_data.size()) throw std::runtime_error("Invalid data: truncated key length"); // Read key std::uint16_t key_len; std::memcpy(&key_len, &binary_data[pos], sizeof(std::uint16_t)); pos += sizeof(std::uint16_t); if (pos + key_len > binary_data.size()) throw std::runtime_error("Invalid data: truncated key"); std::string key(reinterpret_cast(&binary_data[pos]), key_len); pos += key_len; if (pos >= binary_data.size()) throw std::runtime_error("Invalid data: missing value type"); // Read value based on type const std::uint8_t value_type = binary_data[pos++]; switch (value_type) { case 1: { // int64 if (pos + sizeof(std::int64_t) > binary_data.size()) throw std::runtime_error("Invalid data: truncated int64 value"); std::int64_t value; std::memcpy(&value, &binary_data[pos], sizeof(std::int64_t)); pos += sizeof(std::int64_t); entry.data[key] = value; break; } case 2: { // double if (pos + sizeof(double) > binary_data.size()) throw std::runtime_error("Invalid data: truncated double value"); double value; std::memcpy(&value, &binary_data[pos], sizeof(double)); pos += sizeof(double); entry.data[key] = value; break; } case 3: { // string if (pos + sizeof(std::uint16_t) > binary_data.size()) throw std::runtime_error("Invalid data: truncated string length"); std::uint16_t str_len; std::memcpy(&str_len, &binary_data[pos], sizeof(std::uint16_t)); pos += sizeof(std::uint16_t); if (pos + str_len > binary_data.size()) throw std::runtime_error("Invalid data: truncated string value"); std::string value(reinterpret_cast(&binary_data[pos]), str_len); pos += str_len; entry.data[key] = value; break; } default: { throw std::runtime_error(std::format("Unknown value type: {}", static_cast(value_type))); } } } return entry; } // Binary serialization implementation for RepoInfo std::vector RepoInfo::serialize() const { return serializeVariantMap(data); } RepoInfo RepoInfo::deserialize(const std::vector &binary_data) { return deserializeVariantMap(binary_data, false); } // Binary serialization implementation for StatisticsEntry std::vector StatisticsEntry::serialize() const { return serializeVariantMap(data, time); } StatisticsEntry StatisticsEntry::deserialize(const std::vector &binary_data) { return deserializeVariantMap(binary_data, true); } DataStore::DataStore() : m_dbEnv(nullptr), m_dbRepoInfo(0), m_dbPackages(0), m_dbDataXml(0), m_dbDataYaml(0), m_dbHints(0), m_dbStats(0), m_opened(false), m_mdata(nullptr) { m_mdata = as_metadata_new(); as_metadata_set_locale(m_mdata, "ALL"); as_metadata_set_format_version(m_mdata, Config::get().formatVersion); as_metadata_set_write_header(m_mdata, FALSE); } DataStore::~DataStore() { close(); g_object_unref(m_mdata); } const fs::path &DataStore::mediaExportPoolDir() const { return m_mediaDir; } void DataStore::checkError(int rc, const std::string &msg) { if (rc != 0) { throw std::runtime_error(std::format("{}[{}]: {}", msg, rc, mdb_strerror(rc))); } } void DataStore::printVersionDbg() { int major, minor, patch; const char *ver = mdb_version(&major, &minor, &patch); logDebug("Using {} major={} minor={} patch={}", ver, major, minor, patch); } void DataStore::open(const std::string &dir, const fs::path &mediaBaseDir) { std::lock_guard lock(m_mutex); if (m_opened) throw std::runtime_error("DataStore is already opened"); int rc; // add LMDB version we are using to the debug output printVersionDbg(); // ensure the cache directory exists fs::create_directories(dir); rc = mdb_env_create(&m_dbEnv); if (rc != 0) checkError(rc, "mdb_env_create"); // We are going to use at max 6 sub-databases: // packages, hints, metadata_xml, metadata_yaml, statistics, repository rc = mdb_env_set_maxdbs(m_dbEnv, 6); if (rc != 0) { mdb_env_close(m_dbEnv); checkError(rc, "mdb_env_set_maxdbs"); } // set a huge map size to be futureproof. // This means we're cruel to non-64bit users, but this // software is supposed to be run on 64bit machines anyway. auto mapsize = static_cast(std::pow(512L, 4)); rc = mdb_env_set_mapsize(m_dbEnv, mapsize); if (rc != 0) { mdb_env_close(m_dbEnv); checkError(rc, "mdb_env_set_mapsize"); } // open database rc = mdb_env_open(m_dbEnv, dir.c_str(), MDB_NOMETASYNC, 0755); if (rc != 0) { mdb_env_close(m_dbEnv); checkError(rc, "mdb_env_open"); } // open sub-databases in the environment MDB_txn *txn; rc = mdb_txn_begin(m_dbEnv, nullptr, 0, &txn); if (rc != 0) { mdb_env_close(m_dbEnv); checkError(rc, "mdb_txn_begin"); } try { rc = mdb_dbi_open(txn, "packages", MDB_CREATE, &m_dbPackages); checkError(rc, "open packages database"); rc = mdb_dbi_open(txn, "repository", MDB_CREATE, &m_dbRepoInfo); checkError(rc, "open repository database"); rc = mdb_dbi_open(txn, "metadata_xml", MDB_CREATE, &m_dbDataXml); checkError(rc, "open metadata (xml) database"); rc = mdb_dbi_open(txn, "metadata_yaml", MDB_CREATE, &m_dbDataYaml); checkError(rc, "open metadata (yaml) database"); rc = mdb_dbi_open(txn, "hints", MDB_CREATE, &m_dbHints); checkError(rc, "open hints database"); rc = mdb_dbi_open(txn, "statistics", MDB_CREATE | MDB_INTEGERKEY, &m_dbStats); checkError(rc, "open statistics database"); rc = mdb_txn_commit(txn); checkError(rc, "mdb_txn_commit"); } catch (...) { mdb_txn_abort(txn); mdb_env_close(m_dbEnv); throw; } m_opened = true; m_mediaDir = mediaBaseDir / "pool"; fs::create_directories(m_mediaDir); } void DataStore::open(const Config &conf) { open(conf.databaseDir() / "main", conf.mediaExportDir); } void DataStore::close() { std::lock_guard lock(m_mutex); if (m_opened) { mdb_env_close(m_dbEnv); m_opened = false; m_dbEnv = nullptr; } } MDB_val DataStore::makeDbValue(const std::string &data) { // NOTE: We need to be careful about string lifetime // The caller must ensure the string remains valid while MDB_val is in use MDB_val mval; mval.mv_size = data.length() + 1; // include null terminator mval.mv_data = const_cast(data.c_str()); return mval; } MDB_txn *DataStore::newTransaction(unsigned int flags) { if (!m_opened) throw std::runtime_error("DataStore is not opened"); MDB_txn *txn; int rc = mdb_txn_begin(m_dbEnv, nullptr, flags, &txn); checkError(rc, "mdb_txn_begin"); return txn; } void DataStore::commitTransaction(MDB_txn *txn) { int rc = mdb_txn_commit(txn); checkError(rc, "mdb_txn_commit"); } void DataStore::quitTransaction(MDB_txn *txn) { if (!txn) return; mdb_txn_abort(txn); } void DataStore::putKeyValue(MDB_dbi dbi, const std::string &key, const std::string &value) { MDB_val dbkey = makeDbValue(key); MDB_val dbvalue = makeDbValue(value); MDB_txn *txn = newTransaction(); try { int res = mdb_put(txn, dbi, &dbkey, &dbvalue, 0); checkError(res, "mdb_put"); commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } std::string DataStore::getValue(MDB_dbi dbi, MDB_val dkey) { MDB_val dval; MDB_cursor *cur; MDB_txn *txn = newTransaction(MDB_RDONLY); try { int res = mdb_cursor_open(txn, dbi, &cur); checkError(res, "mdb_cursor_open"); res = mdb_cursor_get(cur, &dkey, &dval, MDB_SET); if (res == MDB_NOTFOUND) { mdb_cursor_close(cur); quitTransaction(txn); return {}; } checkError(res, "mdb_cursor_get"); if (dval.mv_data == nullptr || dval.mv_size == 0) { mdb_cursor_close(cur); quitTransaction(txn); return {}; } std::string result(static_cast(dval.mv_data), dval.mv_size - 1); // exclude null terminator mdb_cursor_close(cur); quitTransaction(txn); return result; } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } std::string DataStore::getValue(MDB_dbi dbi, const std::string &key) { MDB_val dkey = makeDbValue(key); return getValue(dbi, dkey); } bool DataStore::metadataExists(DataType dtype, const std::string &gcid) { return !getMetadata(dtype, gcid).empty(); } void DataStore::setMetadata(DataType dtype, const std::string &gcid, const std::string &asdata) { if (dtype == DataType::XML) putKeyValue(m_dbDataXml, gcid, asdata); else putKeyValue(m_dbDataYaml, gcid, asdata); } std::string DataStore::getMetadata(DataType dtype, const std::string &gcid) { if (dtype == DataType::XML) return getValue(m_dbDataXml, gcid); else return getValue(m_dbDataYaml, gcid); } bool DataStore::hasHints(const std::string &pkid) { return !getValue(m_dbHints, pkid).empty(); } void DataStore::setHints(const std::string &pkid, const std::string &hintsJson) { putKeyValue(m_dbHints, pkid, hintsJson); } std::string DataStore::getHints(const std::string &pkid) { return getValue(m_dbHints, pkid); } std::string DataStore::getPackageValue(const std::string &pkid) { return getValue(m_dbPackages, pkid); } void DataStore::setPackageIgnore(const std::string &pkid) { putKeyValue(m_dbPackages, pkid, "ignore"); } bool DataStore::isIgnored(const std::string &pkid) { const auto val = getValue(m_dbPackages, pkid); return val == "ignore"; } bool DataStore::packageExists(const std::string &pkid) { return !getValue(m_dbPackages, pkid).empty(); } void DataStore::addGeneratorResult(DataType dtype, GeneratorResult &gres, bool alwaysRegenerate) { // if the package has no components or hints, // mark it as always-ignore if (gres.isUnitIgnored()) { setPackageIgnore(gres.pkid()); return; } g_autoptr(GPtrArray) cptsArray = gres.fetchComponents(); for (guint i = 0; i < cptsArray->len; i++) { AsComponent *cpt = AS_COMPONENT(cptsArray->pdata[i]); const auto gcid = gres.gcidForComponent(cpt); if (metadataExists(dtype, gcid) && !alwaysRegenerate) { // we already have seen this exact metadata - only adjust the reference, // and don't regenerate it. continue; } std::lock_guard lock(m_mutex); as_metadata_clear_components(m_mdata); as_metadata_add_component(m_mdata, cpt); // convert our component into metadata std::string data; try { g_autoptr(GError) error = nullptr; g_autofree gchar *metadataStr = nullptr; if (dtype == DataType::XML) metadataStr = as_metadata_components_to_catalog(m_mdata, AS_FORMAT_KIND_XML, &error); else metadataStr = as_metadata_components_to_catalog(m_mdata, AS_FORMAT_KIND_YAML, &error); if (error != nullptr) { gres.addHint(cpt, "metadata-serialization-failed", error->message); continue; } if (metadataStr != nullptr) { data = metadataStr; // remove trailing whitespaces and linebreaks data = Utils::rtrimString(data); } } catch (const std::exception &e) { gres.addHint(cpt, "metadata-serialization-failed", e.what()); continue; } // store metadata if (!data.empty()) setMetadata(dtype, gcid, data); } if (gres.hintsCount() > 0) { const auto hintsJson = gres.hintsToJson(); if (!hintsJson.empty()) setHints(gres.pkid(), hintsJson); } const auto gcids = gres.getComponentGcids(); if (gcids.empty()) { // no global components, and we're not ignoring this component. // this means we likely have hints stored for this one. Mark it // as "seen" so we don't reprocess it again. putKeyValue(m_dbPackages, gres.pkid(), "seen"); } else { // store global component IDs for this package as newline-separated list std::string gcidVal = Utils::joinStrings(gcids, "\n"); putKeyValue(m_dbPackages, gres.pkid(), gcidVal); } } std::vector DataStore::getGCIDsForPackage(const std::string &pkid) { const auto pkval = getPackageValue(pkid); if (pkval == "ignore" || pkval == "seen") { return {}; } std::vector validCids; const auto cids = Utils::splitString(pkval, '\n'); for (const auto &cid : cids) { if (!cid.empty()) validCids.push_back(cid); } return validCids; } std::vector DataStore::getMetadataForPackage(DataType dtype, const std::string &pkid) { const auto gcids = getGCIDsForPackage(pkid); if (gcids.empty()) return {}; std::vector result; result.reserve(gcids.size()); for (const auto &cid : gcids) { const auto data = getMetadata(dtype, cid); if (!data.empty()) result.push_back(data); } return result; } void DataStore::removePackage(const std::string &pkid) { MDB_val dbkey = makeDbValue(pkid); MDB_txn *txn = newTransaction(); try { int res = mdb_del(txn, m_dbPackages, &dbkey, nullptr); if (res != MDB_NOTFOUND) { checkError(res, "mdb_del"); } res = mdb_del(txn, m_dbHints, &dbkey, nullptr); if (res != MDB_NOTFOUND) { checkError(res, "mdb_del"); } commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } std::unordered_set DataStore::getActiveGCIDs() { MDB_val dkey, dval; MDB_cursor *cur = nullptr; MDB_txn *txn = newTransaction(MDB_RDONLY); try { int res = mdb_cursor_open(txn, m_dbPackages, &cur); checkError(res, "mdb_cursor_open (gcids)"); std::unordered_set gcids; while (mdb_cursor_get(cur, &dkey, &dval, MDB_NEXT) == 0) { const std::string pkval(static_cast(dval.mv_data), dval.mv_size - 1); if (pkval == "ignore" || pkval == "seen") continue; const auto gcidList = Utils::splitString(pkval, '\n'); for (const auto &gcid : gcidList) { if (!gcid.empty()) gcids.insert(gcid); } } mdb_cursor_close(cur); quitTransaction(txn); return gcids; } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } std::unordered_map> DataStore::getPackagesForGCIDs( std::unordered_set gcids) { MDB_val dkey, dval; MDB_cursor *cur = nullptr; std::unordered_map> result; MDB_txn *txn = newTransaction(MDB_RDONLY); try { int res = mdb_cursor_open(txn, m_dbPackages, &cur); checkError(res, "mdb_cursor_open (gcids)"); while (mdb_cursor_get(cur, &dkey, &dval, MDB_NEXT) == 0) { const std::string pkval(static_cast(dval.mv_data), dval.mv_size - 1); if (pkval == "ignore" || pkval == "seen") continue; const std::string pkid(static_cast(dkey.mv_data), dkey.mv_size - 1); const auto gcidList = Utils::splitString(pkval, '\n'); for (const auto &gcid : gcidList) { if (gcids.contains(gcid)) { if (result.contains(pkid)) result[pkid].push_back(gcid); else result[pkid] = {gcid}; } } } mdb_cursor_close(cur); quitTransaction(txn); return result; } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } void DataStore::dropOrphanedData(MDB_dbi dbi, const std::unordered_set &activeGCIDs) { MDB_cursor *cur = nullptr; MDB_txn *txn = newTransaction(); try { int res = mdb_cursor_open(txn, dbi, &cur); checkError(res, "mdb_cursor_open (stats)"); MDB_val ckey; while (mdb_cursor_get(cur, &ckey, nullptr, MDB_NEXT) == 0) { const std::string gcid(static_cast(ckey.mv_data), ckey.mv_size - 1); if (activeGCIDs.contains(gcid)) { continue; } // if we got here, the component is cruft and can be removed res = mdb_cursor_del(cur, 0); checkError(res, "mdb_del"); logInfo("Marked {} as cruft.", gcid); } mdb_cursor_close(cur); commitTransaction(txn); } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } void DataStore::cleanupDirs(const std::string &rootPath) { auto pdir = fs::path(rootPath).parent_path(); if (!fs::exists(pdir)) return; if (Utils::dirEmpty(pdir)) fs::remove(pdir); pdir = pdir.parent_path(); if (Utils::dirEmpty(pdir)) fs::remove(pdir); } void DataStore::cleanupCruft() { if (m_mediaDir.empty()) { logError("Can not clean up cruft: No media directory is set."); return; } const auto activeGCIDs = getActiveGCIDs(); // drop orphaned metadata dropOrphanedData(m_dbDataXml, activeGCIDs); dropOrphanedData(m_dbDataYaml, activeGCIDs); // we need the global Config instance here const auto &conf = Config::get(); const auto mdirLen = m_mediaDir.string().length(); if (!fs::exists(m_mediaDir)) { logInfo("Media directory '{}' does not exist.", m_mediaDir.string()); return; } // Collect all directory paths first to avoid modifying filesystem while iterating std::vector dirsToProcess; try { for (const auto &entry : fs::recursive_directory_iterator(m_mediaDir, fs::directory_options::skip_permission_denied)) { if (!entry.is_directory()) continue; const auto &path = entry.path(); if (path.string().length() <= mdirLen) continue; const std::string relPath = path.string().substr(mdirLen + 1); const auto pathParts = Utils::splitString(relPath, '/'); if (pathParts.size() != 4) continue; dirsToProcess.push_back(path); } } catch (const fs::filesystem_error &e) { logWarning("Error while scanning media directory: {}", e.what()); return; } // Now process the collected directories for (const auto &path : dirsToProcess) { const std::string relPath = path.string().substr(mdirLen + 1); const std::string &gcid = relPath; if (activeGCIDs.contains(gcid)) continue; // if we are here, the component is removed and we can drop its media if (fs::exists(path)) fs::remove_all(path); // remove possibly empty directories cleanupDirs(path); // expire data in suite-specific media directories, // if suite is not marked as immutable if (conf.feature.immutableSuites) { for (const auto &suite : conf.suites) { if (suite.isImmutable) continue; const auto suiteGCIDMediaDir = m_mediaDir.parent_path() / suite.name / gcid; if (fs::exists(suiteGCIDMediaDir)) fs::remove_all(suiteGCIDMediaDir); // remove possibly empty directories cleanupDirs(suiteGCIDMediaDir); } } logInfo("Expired media for '{}'", gcid); } } std::unordered_set DataStore::getPackageIdSet() { MDB_cursor *cur = nullptr; MDB_txn *txn = newTransaction(); try { std::unordered_set pkgSet; int res = mdb_cursor_open(txn, m_dbPackages, &cur); checkError(res, "mdb_cursor_open (getPackageIdSet)"); MDB_val pkey; while (mdb_cursor_get(cur, &pkey, nullptr, MDB_NEXT) == 0) { const std::string pkid(static_cast(pkey.mv_data), pkey.mv_size - 1); pkgSet.insert(pkid); } mdb_cursor_close(cur); quitTransaction(txn); return pkgSet; } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } void DataStore::removePackages(const std::unordered_set &pkidSet) { MDB_txn *txn = newTransaction(); try { for (const auto &pkid : pkidSet) { MDB_val dbkey = makeDbValue(pkid); int res = mdb_del(txn, m_dbPackages, &dbkey, nullptr); if (res != MDB_NOTFOUND) checkError(res, "mdb_del (metadata)"); res = mdb_del(txn, m_dbHints, &dbkey, nullptr); if (res != MDB_NOTFOUND) checkError(res, "mdb_del (hints)"); logInfo("Dropped package {}", pkid); } commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } void DataStore::putBinaryValue(MDB_dbi dbi, const std::string &key, const std::vector &value) { MDB_val dbkey = makeDbValue(key); MDB_val dbvalue; dbvalue.mv_size = value.size(); dbvalue.mv_data = const_cast(value.data()); MDB_txn *txn = newTransaction(); try { int res = mdb_put(txn, dbi, &dbkey, &dbvalue, 0); checkError(res, "mdb_put"); commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } std::vector DataStore::getBinaryValue(MDB_dbi dbi, const std::string &key) { MDB_val dbkey = makeDbValue(key); MDB_val dval; MDB_cursor *cur = nullptr; MDB_txn *txn = newTransaction(MDB_RDONLY); try { int res = mdb_cursor_open(txn, dbi, &cur); checkError(res, "mdb_cursor_open"); res = mdb_cursor_get(cur, &dbkey, &dval, MDB_SET); if (res == MDB_NOTFOUND) { mdb_cursor_close(cur); quitTransaction(txn); return {}; } checkError(res, "mdb_cursor_get"); std::vector result( static_cast(dval.mv_data), static_cast(dval.mv_data) + dval.mv_size); mdb_cursor_close(cur); quitTransaction(txn); return result; } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } std::vector DataStore::getStatistics() { MDB_val dkey, dval; MDB_cursor *cur = nullptr; MDB_txn *txn = newTransaction(MDB_RDONLY); try { int res = mdb_cursor_open(txn, m_dbStats, &cur); checkError(res, "mdb_cursor_open (stats)"); std::vector stats; stats.reserve(256); while (mdb_cursor_get(cur, &dkey, &dval, MDB_NEXT) == 0) { std::vector binaryData( static_cast(dval.mv_data), static_cast(dval.mv_data) + dval.mv_size); if (!binaryData.empty() && binaryData[0] == '{') { // previously, data was stored in JSON, instead of reading that data, we ignore it now continue; } try { auto entry = StatisticsEntry::deserialize(binaryData); stats.push_back(std::move(entry)); } catch (const std::exception &e) { logWarning("Failed to deserialize statistics entry: {}", e.what()); continue; } } mdb_cursor_close(cur); quitTransaction(txn); return stats; } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } void DataStore::removeStatistics(std::size_t time) { MDB_val dbkey; dbkey.mv_size = sizeof(std::size_t); dbkey.mv_data = const_cast(&time); MDB_txn *txn = newTransaction(); try { int res = mdb_del(txn, m_dbStats, &dbkey, nullptr); if (res != MDB_NOTFOUND) checkError(res, "mdb_del"); commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } void DataStore::addStatistics(const StatisticsEntry &stats) { MDB_val dbkey; dbkey.mv_size = sizeof(std::size_t); dbkey.mv_data = const_cast(&stats.time); auto serializedData = stats.serialize(); MDB_val dbvalue; dbvalue.mv_size = serializedData.size(); dbvalue.mv_data = serializedData.data(); MDB_txn *txn = newTransaction(); try { int res = mdb_put(txn, m_dbStats, &dbkey, &dbvalue, MDB_APPEND); if (res == MDB_KEYEXIST) { // this point in time already exists, so we need to extend it with additional data logWarning("Statistics entry for timestamp {} already exists, overwriting", stats.time); res = mdb_put(txn, m_dbStats, &dbkey, &dbvalue, 0); } checkError(res, "mdb_put (stats)"); commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } void DataStore::addStatistics( const std::unordered_map> &statsData) { StatisticsEntry entry; entry.time = std::time(nullptr); entry.data = statsData; addStatistics(entry); } RepoInfo DataStore::getRepoInfo(const std::string &suite, const std::string §ion, const std::string &arch) { const auto repoid = suite + "-" + section + "-" + arch; const auto binaryData = getBinaryValue(m_dbRepoInfo, repoid); if (binaryData.empty()) return RepoInfo{}; try { return RepoInfo::deserialize(binaryData); } catch (const std::exception &e) { logWarning("Failed to deserialize repository info for {}: {}", repoid, e.what()); return RepoInfo{}; } } void DataStore::setRepoInfo( const std::string &suite, const std::string §ion, const std::string &arch, const RepoInfo &repoInfo) { const auto repoid = suite + "-" + section + "-" + arch; const auto serializedData = repoInfo.serialize(); putBinaryValue(m_dbRepoInfo, repoid, serializedData); } void DataStore::removeRepoInfo(const std::string &suite, const std::string §ion, const std::string &arch) { const auto repoid = suite + "-" + section + "-" + arch; MDB_val dbkey = makeDbValue(repoid); MDB_txn *txn = newTransaction(); try { int res = mdb_del(txn, m_dbRepoInfo, &dbkey, nullptr); if (res != MDB_NOTFOUND) { checkError(res, "mdb_del"); } commitTransaction(txn); } catch (...) { quitTransaction(txn); throw; } } std::vector DataStore::getPkidsMatching(const std::string &prefix) { MDB_val dkey; MDB_cursor *cur = nullptr; MDB_txn *txn = newTransaction(MDB_RDONLY); try { int res = mdb_cursor_open(txn, m_dbPackages, &cur); checkError(res, "mdb_cursor_open (pkid-match)"); std::vector pkids; const std::string searchPrefix = prefix + "/"; while (mdb_cursor_get(cur, &dkey, nullptr, MDB_NEXT) == 0) { const std::string pkid(static_cast(dkey.mv_data), dkey.mv_size - 1); if (pkid.starts_with(searchPrefix)) pkids.push_back(pkid); } mdb_cursor_close(cur); quitTransaction(txn); return pkids; } catch (...) { if (cur) mdb_cursor_close(cur); quitTransaction(txn); throw; } } } // namespace ASGenerator appstream-generator-0.10.1/src/datastore.h000066400000000000000000000175201506754475600205270ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include "config.h" namespace ASGenerator { class GeneratorResult; /** * Statistics entry */ struct StatisticsEntry { std::size_t time; std::unordered_map> data; std::vector serialize() const; static StatisticsEntry deserialize(const std::vector &binary_data); }; /** * Repository info entry */ struct RepoInfo { std::unordered_map> data; std::vector serialize() const; static RepoInfo deserialize(const std::vector &binary_data); }; /** * Main database containing information about scanned packages, * the components they provide, the component metadata itself, * issues found as well as statistics about the metadata evolution * over time. */ class DataStore { public: DataStore(); ~DataStore(); // Delete copy constructor and assignment operator DataStore(const DataStore &) = delete; DataStore &operator=(const DataStore &) = delete; /** * Get the media export pool directory */ const fs::path &mediaExportPoolDir() const; /** * Open database with explicit directories */ void open(const std::string &dir, const fs::path &mediaBaseDir); /** * Open database using configuration */ void open(const Config &conf); /** * Close the database */ void close(); /** * Check if metadata exists for given type and GCID */ bool metadataExists(DataType dtype, const std::string &gcid); /** * Set metadata for given type and GCID */ void setMetadata(DataType dtype, const std::string &gcid, const std::string &asdata); /** * Get metadata for given type and GCID */ std::string getMetadata(DataType dtype, const std::string &gcid); /** * Check if package has hints */ bool hasHints(const std::string &pkid); /** * Set hints for package */ void setHints(const std::string &pkid, const std::string &hintsJson); /** * Get hints for package */ std::string getHints(const std::string &pkid); /** * Get package value from database */ std::string getPackageValue(const std::string &pkid); /** * Mark package as ignored */ void setPackageIgnore(const std::string &pkid); /** * Check if package is ignored */ bool isIgnored(const std::string &pkid); /** * Check if package exists in database */ bool packageExists(const std::string &pkid); /** * Add generator result to database */ void addGeneratorResult(DataType dtype, GeneratorResult &gres, bool alwaysRegenerate = false); /** * Get global component IDs for package */ std::vector getGCIDsForPackage(const std::string &pkid); /** * Get metadata strings for package */ std::vector getMetadataForPackage(DataType dtype, const std::string &pkid); /** * Drop a package from the database. This process might leave cruft behind, * which can be collected using the cleanupCruft() method. */ void removePackage(const std::string &pkid); /** * Clean up orphaned data and media files */ void cleanupCruft(); /** * Get map of package-IDs to global component IDs based on given GCID list */ std::unordered_map> getPackagesForGCIDs( std::unordered_set gcids); /** * Get set of all package IDs in database */ std::unordered_set getPackageIdSet(); /** * Remove multiple packages from database */ void removePackages(const std::unordered_set &pkidSet); /** * Get all statistics entries */ std::vector getStatistics(); /** * Remove statistics entry for given time */ void removeStatistics(std::size_t time); /** * Add statistics entry */ void addStatistics(const StatisticsEntry &stats); /** * Add statistics entry from key-value data */ void addStatistics( const std::unordered_map> &statsData); /** * Get repository info */ RepoInfo getRepoInfo(const std::string &suite, const std::string §ion, const std::string &arch); /** * Set repository info */ void setRepoInfo( const std::string &suite, const std::string §ion, const std::string &arch, const RepoInfo &repoInfo); /** * Remove repository info */ void removeRepoInfo(const std::string &suite, const std::string §ion, const std::string &arch); /** * Get a list of package-ids which match a prefix. */ std::vector getPkidsMatching(const std::string &prefix); private: MDB_env *m_dbEnv; MDB_dbi m_dbRepoInfo; MDB_dbi m_dbPackages; MDB_dbi m_dbDataXml; MDB_dbi m_dbDataYaml; MDB_dbi m_dbHints; MDB_dbi m_dbStats; bool m_opened; AsMetadata *m_mdata; fs::path m_mediaDir; mutable std::mutex m_mutex; /** * Check LMDB error and throw exception if needed */ void checkError(int rc, const std::string &msg); /** * Print LMDB version debug info */ void printVersionDbg(); /** * Create MDB_val from string data */ MDB_val makeDbValue(const std::string &data); /** * Create new LMDB transaction */ MDB_txn *newTransaction(unsigned int flags = 0); /** * Commit LMDB transaction */ void commitTransaction(MDB_txn *txn); /** * Abort LMDB transaction */ void quitTransaction(MDB_txn *txn); /** * Put key-value pair into database */ void putKeyValue(MDB_dbi dbi, const std::string &key, const std::string &value); /** * Get value from database using MDB_val key */ std::string getValue(MDB_dbi dbi, MDB_val dkey); /** * Get value from database using string key */ std::string getValue(MDB_dbi dbi, const std::string &key); /** * Get active global component IDs */ std::unordered_set getActiveGCIDs(); /** * Drop orphaned data from given database */ void dropOrphanedData(MDB_dbi dbi, const std::unordered_set &activeGCIDs); /** * Clean up empty directories */ void cleanupDirs(const std::string &rootPath); /** * Put binary value into database */ void putBinaryValue(MDB_dbi dbi, const std::string &key, const std::vector &value); /** * Get binary value from database */ std::vector getBinaryValue(MDB_dbi dbi, const std::string &key); }; } // namespace ASGenerator appstream-generator-0.10.1/src/dataunits.cpp000066400000000000000000000331771506754475600210760ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "dataunits.h" #include #include #include #include #include #include #include "backends/interfaces.h" #include "contentsstore.h" #include "logging.h" #include "config.h" using namespace ASGenerator; /* AsgPackageUnit implementation */ /** * AsgPackageUnit - A unit representing a single package */ struct _AsgPackageUnit { AscUnit parent_instance; /* Private data stored as C++ objects */ gpointer priv_data; }; namespace { /** * Private data structure for AsgPackageUnit */ class PackageUnitPrivate { public: std::shared_ptr package; mutable std::shared_mutex mutex; bool contents_loaded = false; explicit PackageUnitPrivate(std::shared_ptr pkg) : package(std::move(pkg)) { } }; } // anonymous namespace G_DEFINE_TYPE(AsgPackageUnit, asg_package_unit, ASC_TYPE_UNIT) /** * asg_package_unit_new: * @pkg: Package to wrap (ownership is transferred) * * Create a new package unit for a given package. */ AsgPackageUnit *asg_package_unit_new(std::shared_ptr pkg) { AsgPackageUnit *unit = static_cast(g_object_new(ASG_TYPE_PACKAGE_UNIT, nullptr)); unit->priv_data = new PackageUnitPrivate(pkg); // set identity asc_unit_set_bundle_id(ASC_UNIT(unit), pkg->name().c_str()); asc_unit_set_bundle_kind(ASC_UNIT(unit), AS_BUNDLE_KIND_PACKAGE); return unit; } static gboolean asg_package_unit_open_impl(AscUnit *unit, GError **error) { AsgPackageUnit *pkg_unit = ASG_PACKAGE_UNIT(unit); auto *priv = static_cast(pkg_unit->priv_data); if (!priv || !priv->package) { g_set_error(error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "No package associated with this unit."); return FALSE; } std::unique_lock lock(priv->mutex); try { // Load package contents const auto &contents = priv->package->contents(); // Set contents in the parent AscUnit g_autoptr(GPtrArray) contents_array = g_ptr_array_new_with_free_func(g_free); for (const auto &filename : contents) g_ptr_array_add(contents_array, g_strdup(filename.c_str())); asc_unit_set_contents(unit, contents_array); priv->contents_loaded = true; return TRUE; } catch (const std::exception &e) { logError("Failed to open package unit: {}", e.what()); g_set_error(error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "Failed to open package unit: %s", e.what()); return FALSE; } } static void asg_package_unit_close_impl(AscUnit *unit) { AsgPackageUnit *pkg_unit = ASG_PACKAGE_UNIT(unit); auto *priv = static_cast(pkg_unit->priv_data); if (priv->package) priv->package->finish(); } static gboolean asg_package_unit_dir_exists_impl(AscUnit *unit, const gchar *dirname) { AsgPackageUnit *pkg_unit = ASG_PACKAGE_UNIT(unit); auto *priv = static_cast(pkg_unit->priv_data); if (!priv || !priv->package) { g_warning("No package associated with this unit."); return FALSE; } std::shared_lock lock(priv->mutex); if (!priv->contents_loaded) { g_warning("Package contents not loaded yet."); return FALSE; } const std::string dirpath(dirname); const std::string dirpath_slash = dirpath + "/"; for (const auto &file : priv->package->contents()) { if (file.starts_with(dirpath_slash)) return TRUE; } return FALSE; } static GBytes *asg_package_unit_read_data_impl(AscUnit *unit, const gchar *filename, GError **error) { AsgPackageUnit *pkg_unit = ASG_PACKAGE_UNIT(unit); auto *priv = static_cast(pkg_unit->priv_data); if (!priv || !priv->package) { g_set_error(error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "No package associated with this unit."); return nullptr; } std::shared_lock lock(priv->mutex); try { const std::string fname(filename); auto data = priv->package->getFileData(fname); if (data.empty()) { g_set_error( error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "File '%s' does not exist or is empty.", filename); return nullptr; } // Create a copy of the data for GBytes void *data_copy = g_memdup2(data.data(), data.size()); return g_bytes_new_take(data_copy, data.size()); } catch (const std::exception &e) { logError("Failed to read data from package unit: {}", e.what()); g_set_error(error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "Failed to read data: %s", e.what()); return nullptr; } } static void asg_package_unit_finalize(GObject *object) { AsgPackageUnit *pkg_unit = ASG_PACKAGE_UNIT(object); if (pkg_unit->priv_data) { auto *priv = static_cast(pkg_unit->priv_data); delete priv; pkg_unit->priv_data = nullptr; } G_OBJECT_CLASS(asg_package_unit_parent_class)->finalize(object); } static void asg_package_unit_class_init(AsgPackageUnitClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); AscUnitClass *unit_class = ASC_UNIT_CLASS(klass); object_class->finalize = asg_package_unit_finalize; unit_class->open = asg_package_unit_open_impl; unit_class->close = asg_package_unit_close_impl; unit_class->dir_exists = asg_package_unit_dir_exists_impl; unit_class->read_data = asg_package_unit_read_data_impl; } static void asg_package_unit_init(AsgPackageUnit *pkg_unit) { pkg_unit->priv_data = nullptr; } /* AsgLocaleUnit implementation */ /** * AsgLocaleUnit - A unit for handling locale-specific files across multiple packages */ struct _AsgLocaleUnit { AscUnit parent_instance; /* Private data stored as C++ objects */ gpointer priv_data; }; namespace { /** * Private data structure for AsgLocaleUnit */ class LocaleUnitPrivate { public: std::shared_ptr contents_store; std::vector> package_list; std::unordered_map locale_file_pkg_map; mutable std::shared_mutex mutex; explicit LocaleUnitPrivate(std::shared_ptr cstore, const std::vector> &pkgs) : contents_store(std::move(cstore)) { package_list = pkgs; // Check if locale processing is enabled const auto &conf = Config::get(); if (!conf.feature.processLocale) { // Don't load the expensive locale<->package mapping if we don't need it return; } // Convert the list into a map for faster lookups std::unordered_map pkgMap; for (const auto &pkg : package_list) { const std::string pkid = pkg->id(); pkgMap[pkid] = pkg.get(); } // Get package IDs for the contents store lookup std::vector pkids; pkids.reserve(pkgMap.size()); for (const auto &[pkid, pkg] : pkgMap) pkids.push_back(pkid); // We make the assumption here that all locale for a given domain are in one package. // Otherwise this global search will get even more insane. // The key of the map returned by getLocaleMap will therefore contain only the locale // file basename instead of a full path auto dbLocaleMap = contents_store->getLocaleMap(pkids); for (const auto &[id, pkgid] : dbLocaleMap) { // Check if we already have a package - lookups in this HashMap are faster // due to its smaller size and (most of the time) outweigh the following additional // lookup for the right package entity. if (locale_file_pkg_map.find(id) != locale_file_pkg_map.end()) continue; Package *pkg = nullptr; if (!pkgid.empty()) { auto pkgIt = pkgMap.find(pkgid); if (pkgIt != pkgMap.end()) pkg = pkgIt->second; } if (pkg != nullptr) locale_file_pkg_map[id] = pkg; } } }; } // anonymous namespace G_DEFINE_TYPE(AsgLocaleUnit, asg_locale_unit, ASC_TYPE_UNIT) /** * asg_locale_unit_new: * @contents_store: ContentsStore instance (ownership is transferred) * @package_list: Package list (ownership is transferred) * * Create a new locale unit with contents store and package list. */ AsgLocaleUnit *asg_locale_unit_new(std::shared_ptr cstore, std::vector> pkgList) { auto unit = static_cast(g_object_new(ASG_TYPE_LOCALE_UNIT, nullptr)); try { unit->priv_data = new LocaleUnitPrivate(std::move(cstore), pkgList); // Set bundle information for locale unit asc_unit_set_bundle_id(ASC_UNIT(unit), "locale-data"); asc_unit_set_bundle_kind(ASC_UNIT(unit), AS_BUNDLE_KIND_UNKNOWN); } catch (const std::exception &e) { logError("Failed to create locale unit: {}", e.what()); g_object_unref(unit); return nullptr; } return unit; } static gboolean asg_locale_unit_open_impl(AscUnit *unit, GError **error) { AsgLocaleUnit *locale_unit = ASG_LOCALE_UNIT(unit); auto *priv = static_cast(locale_unit->priv_data); if (!priv) { g_set_error(error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "No locale mapping associated with this unit."); return FALSE; } std::shared_lock lock(priv->mutex); // Set up contents list from the file mapping keys g_autoptr(GPtrArray) contents_array = g_ptr_array_new_with_free_func(g_free); for (const auto &[filename, pkg] : priv->locale_file_pkg_map) g_ptr_array_add(contents_array, g_strdup(filename.c_str())); asc_unit_set_contents(unit, contents_array); return TRUE; } static void asg_locale_unit_close_impl(AscUnit *unit) { // noop - locale units don't need explicit closing } static gboolean asg_locale_unit_dir_exists_impl(AscUnit *unit, const gchar *dirname) { // not implemented yet, as it's not needed for locale finding (yet?) return FALSE; } static GBytes *asg_locale_unit_read_data_impl(AscUnit *unit, const gchar *filename, GError **error) { AsgLocaleUnit *locale_unit = ASG_LOCALE_UNIT(unit); auto *priv = static_cast(locale_unit->priv_data); if (!priv) { g_set_error(error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "No locale mapping associated with this unit."); return nullptr; } std::shared_lock lock(priv->mutex); try { const std::string fname(filename); auto it = priv->locale_file_pkg_map.find(fname); if (it == priv->locale_file_pkg_map.end()) { g_set_error( error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "File '%s' does not exist in a known package!", filename); return nullptr; } Package *pkg = it->second; if (!pkg) { g_set_error(error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "Package for file '%s' is null!", filename); return nullptr; } auto data = pkg->getFileData(fname); if (data.empty()) { g_set_error( error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "File '%s' does not exist or is empty.", filename); return nullptr; } // Create a copy of the data for GBytes void *data_copy = g_memdup2(data.data(), data.size()); return g_bytes_new_take(data_copy, data.size()); } catch (const std::exception &e) { logError("Failed to read data from locale unit: {}", e.what()); g_set_error(error, ASC_COMPOSE_ERROR, ASC_COMPOSE_ERROR_FAILED, "Failed to read data: %s", e.what()); return nullptr; } } static void asg_locale_unit_finalize(GObject *object) { AsgLocaleUnit *locale_unit = ASG_LOCALE_UNIT(object); if (locale_unit->priv_data) { auto *priv = static_cast(locale_unit->priv_data); delete priv; locale_unit->priv_data = nullptr; } G_OBJECT_CLASS(asg_locale_unit_parent_class)->finalize(object); } static void asg_locale_unit_class_init(AsgLocaleUnitClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); AscUnitClass *unit_class = ASC_UNIT_CLASS(klass); object_class->finalize = asg_locale_unit_finalize; unit_class->open = asg_locale_unit_open_impl; unit_class->close = asg_locale_unit_close_impl; unit_class->dir_exists = asg_locale_unit_dir_exists_impl; unit_class->read_data = asg_locale_unit_read_data_impl; } static void asg_locale_unit_init(AsgLocaleUnit *locale_unit) { locale_unit->priv_data = nullptr; } appstream-generator-0.10.1/src/dataunits.h000066400000000000000000000032031506754475600205260ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include "contentsstore.h" #include "backends/interfaces.h" G_BEGIN_DECLS #define ASG_TYPE_PACKAGE_UNIT (asg_package_unit_get_type()) G_DECLARE_FINAL_TYPE(AsgPackageUnit, asg_package_unit, ASG, PACKAGE_UNIT, AscUnit) #define ASG_TYPE_LOCALE_UNIT (asg_locale_unit_get_type()) G_DECLARE_FINAL_TYPE(AsgLocaleUnit, asg_locale_unit, ASG, LOCALE_UNIT, AscUnit) /** * Create a new package unit for a given package. */ AsgPackageUnit *asg_package_unit_new(std::shared_ptr pkg); /** * Create a new locale unit with contents store and package list. */ AsgLocaleUnit *asg_locale_unit_new( std::shared_ptr cstore, std::vector> pkgList); G_END_DECLS appstream-generator-0.10.1/src/downloader.cpp000066400000000000000000000317221506754475600212320ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "downloader.h" #include #include #include #include #include #include #include #include #include #include #include #include "defines.h" #include "config.h" #include "logging.h" #include "utils.h" namespace ASGenerator { // Thread-local instance thread_local std::unique_ptr Downloader::instance_; DownloadException::DownloadException(const std::string &message) : m_message(message) { } const char *DownloadException::what() const noexcept { return m_message.c_str(); } struct WriteCallbackData { std::ofstream *file; std::vector *buffer; }; // Callback function for writing data to file or buffer static size_t writeCallback(void *contents, size_t size, size_t nmemb, void *userData) { size_t totalSize = size * nmemb; WriteCallbackData *data = static_cast(userData); if (data->file && data->file->is_open()) { data->file->write(static_cast(contents), totalSize); return data->file->good() ? totalSize : 0; } else if (data->buffer) { const auto *bytes = static_cast(contents); data->buffer->insert(data->buffer->end(), bytes, bytes + totalSize); return totalSize; } return 0; } // Callback function for header processing struct HeaderCallbackData { bool httpsUrl; std::optional *lastModified; }; static size_t headerCallback(char *buffer, size_t size, size_t nitems, void *userData) { size_t totalSize = size * nitems; HeaderCallbackData *data = static_cast(userData); std::string header(buffer, totalSize); std::transform(header.begin(), header.end(), header.begin(), ::tolower); // Check for HTTPS -> HTTP downgrade if (data->httpsUrl && header.starts_with("location:")) { auto pos = header.find("http:"); if (pos != std::string::npos) throw DownloadException("HTTPS URL tried to redirect to a less secure HTTP URL."); } // Parse Last-Modified header if (header.starts_with("last-modified:")) { auto colonPos = header.find(':'); if (colonPos != std::string::npos) { std::string dateStr = header.substr(colonPos + 1); // Trim whitespace dateStr.erase(0, dateStr.find_first_not_of(" \t")); dateStr.erase(dateStr.find_last_not_of(" \t\r\n") + 1); // Parse RFC822 date format using strptime std::tm tm = {}; if (strptime(dateStr.c_str(), "%a, %d %b %Y %H:%M:%S %Z", &tm)) { auto timeT = std::mktime(&tm); if (timeT != -1) { *(data->lastModified) = std::chrono::system_clock::from_time_t(timeT); } } } } return totalSize; } Downloader &Downloader::get() { if (!instance_) instance_ = std::make_unique(); return *instance_; } Downloader::Downloader() : userAgent(std::format("appstream-generator/{}", std::string(ASGEN_VERSION))), caInfo(Config::get().caInfo) { // Initialize curl globally (should be done once per process) static bool curlInitialized = false; if (!curlInitialized) { curl_global_init(CURL_GLOBAL_DEFAULT); curlInitialized = true; } } std::optional Downloader::downloadInternal( const std::string &url, std::ofstream &dest, std::uint32_t maxTryCount) { if (!Utils::isRemote(url)) throw DownloadException("URL is not remote"); std::optional lastModified; /* the curl library is stupid; you can't make an AutoProtocol set timeouts */ logDebug("Downloading {}", url); CURL *curl = curl_easy_init(); if (!curl) { throw DownloadException("Failed to initialize curl"); } try { WriteCallbackData writeData{&dest, nullptr}; HeaderCallbackData headerData{url.starts_with("https"), &lastModified}; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &writeData); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headerData); curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L); if (!caInfo.empty()) curl_easy_setopt(curl, CURLOPT_CAINFO, caInfo.c_str()); CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { if (maxTryCount > 0) { logDebug( "Failed to download {}, will retry {} more {}", url, maxTryCount, maxTryCount > 1 ? "times" : "time"); // Reset file position to beginning before retry to avoid appending to partial data dest.seekp(0); curl_easy_cleanup(curl); return downloadInternal(url, dest, maxTryCount - 1); } else { curl_easy_cleanup(curl); throw DownloadException(std::format("curl_easy_perform() failed: {}", curl_easy_strerror(res))); } } long responseCode; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode); if (responseCode != 200 && responseCode != 301 && responseCode != 302) { if (responseCode == 0) { // just to be safe, check whether we received data before assuming everything went fine if (dest.tellp() == 0) { curl_easy_cleanup(curl); throw DownloadException( std::format("No data was received from the remote end (Code: {}).", responseCode)); } } else { curl_easy_cleanup(curl); throw DownloadException(std::format("HTTP request returned status code {}", responseCode)); } } curl_easy_cleanup(curl); logDebug("Downloaded {}", url); } catch (const DownloadException &) { curl_easy_cleanup(curl); throw; } catch (const std::exception &e) { if (maxTryCount > 0) { logDebug( "Failed to download {}, will retry {} more {}", url, maxTryCount, maxTryCount > 1 ? "times" : "time"); // Reset file position to beginning before retry to avoid appending to partial data dest.seekp(0); curl_easy_cleanup(curl); return downloadInternal(url, dest, maxTryCount - 1); } else { curl_easy_cleanup(curl); throw DownloadException(e.what()); } } return lastModified; } std::optional Downloader::download( const std::string &url, std::ofstream &dFile, std::uint32_t maxTryCount) { return downloadInternal(url, dFile, maxTryCount); } std::vector Downloader::download(const std::string &url, std::uint32_t maxTryCount) { if (!Utils::isRemote(url)) throw DownloadException("URL is not remote"); std::vector buffer; std::optional lastModified; logDebug("Downloading {}", url); CURL *curl = curl_easy_init(); if (!curl) throw DownloadException("Failed to initialize curl"); try { WriteCallbackData writeData{nullptr, &buffer}; HeaderCallbackData headerData{url.starts_with("https"), &lastModified}; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &writeData); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headerData); curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L); if (!caInfo.empty()) { curl_easy_setopt(curl, CURLOPT_CAINFO, caInfo.c_str()); } CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { if (maxTryCount > 0) { logDebug( "Failed to download {}, will retry {} more {}", url, maxTryCount, maxTryCount > 1 ? "times" : "time"); curl_easy_cleanup(curl); return download(url, maxTryCount - 1); } else { curl_easy_cleanup(curl); throw DownloadException(std::format("curl_easy_perform() failed: {}", curl_easy_strerror(res))); } } long responseCode; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode); if (responseCode != 200 && responseCode != 301 && responseCode != 302) { if (responseCode == 0) { if (buffer.empty()) { curl_easy_cleanup(curl); throw DownloadException( std::format("No data was received from the remote end (Code: {}).", responseCode)); } } else { curl_easy_cleanup(curl); throw DownloadException(std::format("HTTP request returned status code {}", responseCode)); } } curl_easy_cleanup(curl); logDebug("Downloaded {}", url); } catch (const DownloadException &) { curl_easy_cleanup(curl); throw; } catch (const std::exception &e) { if (maxTryCount > 0) { logDebug( "Failed to download {}, will retry {} more {}", url, maxTryCount, maxTryCount > 1 ? "times" : "time"); curl_easy_cleanup(curl); return download(url, maxTryCount - 1); } else { curl_easy_cleanup(curl); throw DownloadException(e.what()); } } return buffer; } void Downloader::downloadFile(const std::string &url, const std::string &dest, std::uint32_t maxTryCount) { if (!Utils::isRemote(url)) throw DownloadException("URL is not remote"); if (fs::exists(dest)) { logDebug("File '{}' already exists, re-download of '{}' skipped.", dest, url); return; } fs::create_directories(fs::path(dest).parent_path()); std::ofstream file(dest, std::ios::binary); if (!file.is_open()) throw DownloadException(std::format("Failed to open destination file: {}", dest)); try { auto lastModified = downloadInternal(url, file, maxTryCount); file.close(); if (lastModified) { // Set file times if we have last-modified information auto timeT = std::chrono::system_clock::to_time_t(*lastModified); auto currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); // Set access and modification times of the source struct timespec times[2]; times[0].tv_sec = currentTime; // access time times[0].tv_nsec = 0; times[1].tv_sec = timeT; // modification time times[1].tv_nsec = 0; utimensat(AT_FDCWD, dest.c_str(), times, 0); } } catch (...) { file.close(); fs::remove(dest); throw; } } std::string Downloader::downloadText(const std::string &url, std::uint32_t maxTryCount) { auto data = download(url, maxTryCount); return std::string(data.begin(), data.end()); } std::vector Downloader::downloadTextLines(const std::string &url, std::uint32_t maxTryCount) { auto text = downloadText(url, maxTryCount); std::vector lines; std::stringstream ss(text); std::string line; while (std::getline(ss, line)) lines.push_back(line); return lines; } } // namespace ASGenerator appstream-generator-0.10.1/src/downloader.h000066400000000000000000000057771506754475600207120ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include namespace ASGenerator { class DownloadException : public std::exception { public: explicit DownloadException(const std::string &message); const char *what() const noexcept override; private: std::string m_message; }; /** * Download data via HTTP. Based on cURL. */ class Downloader { public: /** * Get thread-local singleton instance */ static Downloader &get(); Downloader(); /** * Download to file stream and return last-modified time if available */ std::optional download( const std::string &url, std::ofstream &dFile, std::uint32_t maxTryCount = 4); /** * Download to memory and return data as byte vector */ std::vector download(const std::string &url, std::uint32_t maxTryCount = 4); /** * Download `url` to `dest`. * * Params: * url = The URL to download. * dest = The location for the downloaded file. * maxTryCount = Number of times to attempt the download. */ void downloadFile(const std::string &url, const std::string &dest, std::uint32_t maxTryCount = 4); /** * Download `url` and return a string with its contents. * * Params: * url = The URL to download. * maxTryCount = Number of times to retry on timeout. */ std::string downloadText(const std::string &url, std::uint32_t maxTryCount = 4); /** * Download `url` and return a string array of lines. * * Params: * url = The URL to download. * maxTryCount = Number of times to retry on timeout. */ std::vector downloadTextLines(const std::string &url, std::uint32_t maxTryCount = 4); private: const std::string userAgent; const std::string caInfo; // thread local instance static thread_local std::unique_ptr instance_; std::optional downloadInternal( const std::string &url, std::ofstream &dest, std::uint32_t maxTryCount = 5); }; } // namespace ASGenerator appstream-generator-0.10.1/src/engine.cpp000066400000000000000000001375421506754475600203500ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "defines.h" #include "engine.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datainjectpkg.h" #include "extractor.h" #include "hintregistry.h" #include "logging.h" #include "result.h" #include "utils.h" #include "yaml-utils.h" #include "zarchive.h" #include "backends/interfaces.h" // Backends #include "backends/dummy/pkgindex.h" #include "backends/debian/debpkgindex.h" #include "backends/ubuntu/ubupkgindex.h" #include "backends/alpinelinux/apkpkgindex.h" #include "backends/archlinux/alpkgindex.h" #include "backends/rpmmd/rpmpkgindex.h" #include "backends/freebsd/fbsdpkgindex.h" namespace ASGenerator { Engine::Engine() : m_conf(&Config::get()), m_forced(false) { // Configure a TBB task arena to limit parallelism a little (use half the available CPU cores, or at least 6 // threads) This avoids having too many parallel downloads on high-core-count machines, and also leaves some room // for additional parallelism of the used libraries, e.g. for image processing. const auto numCPU = std::thread::hardware_concurrency(); const auto maxThreads = std::max(numCPU > 6 ? 6L : numCPU, std::lround(numCPU * 0.60)); m_taskArena = std::make_unique(maxThreads); // Select backend switch (m_conf->backend) { case Backend::Dummy: m_pkgIndex = std::make_unique(m_conf->archiveRoot); break; case Backend::Debian: m_pkgIndex = std::make_unique(m_conf->archiveRoot); break; case Backend::Ubuntu: m_pkgIndex = std::make_unique(m_conf->archiveRoot); break; case Backend::Archlinux: m_pkgIndex = std::make_unique(m_conf->archiveRoot); break; case Backend::RpmMd: m_pkgIndex = std::make_unique(m_conf->archiveRoot); break; case Backend::Alpinelinux: m_pkgIndex = std::make_unique(m_conf->archiveRoot); break; case Backend::FreeBSD: m_pkgIndex = std::make_unique(m_conf->archiveRoot); break; default: throw std::runtime_error("No backend specified, can not continue!"); } // Load global registry of issue hint templates loadHintsRegistry(); // Create cache in cache directory on workspace m_dstore = std::make_shared(); m_dstore->open(*m_conf); // Open package contents cache m_cstore = std::make_shared(); m_cstore->open(*m_conf); } bool Engine::forced() const { return m_forced; } void Engine::setForced(bool v) { m_forced = v; } void Engine::logVersionInfo() { std::string backendInfo = ""; if (!m_conf->backendName.empty()) backendInfo = std::format(" [{}]", m_conf->backendName); // Get AppStream version const char *asVersion = as_version_string(); logInfo("AppStream Generator {}, AS: {}{}", ASGEN_VERSION, asVersion, backendInfo); } void Engine::checkLibfyamlVersion() { const auto version = Yaml::libfyamlVersion(); if (version.empty()) { logWarning("Unable to determine libfyaml version! Please ensure you have at least version 0.9 installed!"); return; } if (as_vercmp_simple(version.c_str(), "0.9") < 0) { throw std::runtime_error(std::format( "libfyaml version {} is too old, at least version 0.9 is required! Please upgrade libfyaml, as versions <= " "0.8 have known bugs in unicode handling and may generate corrupted UTF-8 data.", version)); } } void Engine::processPackages( const std::vector> &pkgs, std::shared_ptr iconh, std::shared_ptr injMods) { g_autoptr(AsgLocaleUnit) localeUnit = asg_locale_unit_new(m_cstore, pkgs); { g_autoptr(GError) error = NULL; if (!asc_unit_open(ASC_UNIT(localeUnit), &error)) throw std::runtime_error( std::format("Failed to open locale unit: {}", error ? error->message : "Unknown error")); } const auto numProcessors = std::thread::hardware_concurrency(); std::size_t chunkSize = pkgs.size() / numProcessors / 10; if (chunkSize > 100) chunkSize = 100; if (chunkSize <= 10) chunkSize = 10; logDebug( "Analyzing {} packages in batches of {} with {} parallel tasks", pkgs.size(), chunkSize, m_taskArena->max_concurrency()); m_taskArena->execute([&] { tbb::parallel_for( tbb::blocked_range(0, pkgs.size(), chunkSize), [&](const tbb::blocked_range &range) { auto mde = std::make_unique(m_dstore, iconh, localeUnit, injMods); for (std::size_t i = range.begin(); i != range.end(); ++i) { auto pkg = pkgs[i]; const auto &pkid = pkg->id(); if (m_dstore->packageExists(pkid)) continue; auto res = mde->processPackage(pkg); { std::lock_guard lock(m_mutex); // Write resulting data into the database m_dstore->addGeneratorResult(m_conf->metadataType, res); } logInfo( "Processed {}, components: {}, hints: {}", res.pkid(), res.componentsCount(), res.hintsCount()); // We don't need content data from this package anymore pkg->finish(); } }); }); // not strictly necessary, but let's close the unit explicitly asc_unit_close(ASC_UNIT(localeUnit)); } // Helper to check if a package may contain interesting metadata static bool packageIsInteresting(std::shared_ptr pkg) { // Prefixes are defined as string_view for faster comparison constexpr std::string_view usr_share_apps = "/usr/share/applications/"; constexpr std::string_view usr_share_meta = "/usr/share/metainfo/"; constexpr std::string_view usr_local_apps = "/usr/local/share/applications/"; constexpr std::string_view usr_local_meta = "/usr/local/share/metainfo/"; constexpr std::string_view usr_share = "/usr/share/"; constexpr std::string_view usr_local = "/usr/local/share/"; const auto &contents = pkg->contents(); for (const auto &c : contents) { // Quick length check first - all interesting paths are at least 18 characters if (c.size() < 18) continue; std::string_view path_view{c}; if (path_view.starts_with(usr_share)) { // Already know it starts with /usr/share/, check specific subdirs if (path_view.starts_with(usr_share_apps) || path_view.starts_with(usr_share_meta)) return true; } else if (path_view.starts_with(usr_local)) { // Already know it starts with /usr/local/share/, check specific subdirs if (path_view.starts_with(usr_local_apps) || path_view.starts_with(usr_local_meta)) return true; } } // Check for GStreamer codec info auto gst = pkg->gst(); if (gst.has_value() && gst->isNotEmpty()) return true; return false; } bool Engine::seedContentsData( const Suite &suite, const std::string §ion, const std::string &arch, const std::vector> &pkgs) { const auto numProcessors = std::thread::hardware_concurrency(); std::size_t workUnitSize = numProcessors * 2; if (workUnitSize >= pkgs.size()) workUnitSize = 4; if (workUnitSize > 30) workUnitSize = 30; logDebug( "Scanning {} packages, work unit size: {}, parallel tasks: {}", pkgs.size(), workUnitSize, m_taskArena->max_concurrency()); // Check if the index has changed data, skip the update if there's nothing new if (pkgs.empty() && !m_pkgIndex->hasChanges(m_dstore, suite.name, section, arch) && !m_forced) { logDebug("Skipping contents cache update for {}/{} [{}], index has not changed.", suite.name, section, arch); return false; } logInfo("Scanning new packages for {}/{} [{}]", suite.name, section, arch); std::vector> packagesToProcess = pkgs; if (packagesToProcess.empty()) packagesToProcess = m_pkgIndex->packagesFor(suite.name, section, arch); // Get contents information for packages and add them to the database std::atomic_bool interestingFound = false; // First get the contents (only) of all packages in the base suite if (!suite.baseSuite.empty()) { logInfo("Scanning new packages for base suite {}/{} [{}]", suite.baseSuite, section, arch); auto baseSuitePkgs = m_pkgIndex->packagesFor(suite.baseSuite, section, arch); m_taskArena->execute([&] { tbb::parallel_for( tbb::blocked_range(0, baseSuitePkgs.size(), workUnitSize), [&](const tbb::blocked_range &range) { for (size_t i = range.begin(); i != range.end(); ++i) { auto pkg = baseSuitePkgs[i]; const auto &pkid = pkg->id(); if (!m_cstore->packageExists(pkid)) { m_cstore->addContents(pkid, pkg->contents()); logInfo("Scanned {} for base suite.", pkid); } // Chances are that we might never want to extract data from these packages, so remove their // temporary data for now - we can reopen the packages later if we actually need them. pkg->cleanupTemp(); } }); }); } // And then scan the suite itself - here packages can be 'interesting' // in that they might end up in the output. m_taskArena->execute([&] { tbb::parallel_for( tbb::blocked_range(0, packagesToProcess.size(), workUnitSize), [&](const tbb::blocked_range &range) { for (std::size_t i = range.begin(); i != range.end(); ++i) { auto pkg = packagesToProcess[i]; const auto &pkid = pkg->id(); std::vector contents; if (m_cstore->packageExists(pkid)) { if (m_dstore->packageExists(pkid)) { // TODO: Unfortunately, packages can move between suites without changing their ID. // This means as soon as we have an interesting package, even if we already processed it, // we need to regenerate the output metadata. // For that to happen, we set interestingFound to true here. Later, a more elegant solution // would be desirable here, ideally one which doesn't force us to track which package is // in which suite as well. if (!m_dstore->isIgnored(pkid)) interestingFound.store(true); continue; } // We will complement the main database with ignore data, in case it // went missing. contents = m_cstore->getContents(pkid); } else { // Add contents to the index contents = pkg->contents(); m_cstore->addContents(pkid, contents); } // Check if we can already mark this package as ignored, and print some log messages if (!packageIsInteresting(pkg)) { m_dstore->setPackageIgnore(pkid); logInfo("Scanned {}, no interesting files found.", pkid); // We won't use this anymore pkg->finish(); } else { logInfo("Scanned {}, could be interesting.", pkid); interestingFound.store(true); } } }); }); // Ensure the contents store is in a consistent state on disk, // since it might be accessed from other threads after this function // is run. m_cstore->sync(); return interestingFound; } std::string Engine::getMetadataHead(const Suite &suite, const std::string §ion) { std::string head; auto origin = std::format("{}-{}-{}", m_conf->projectName, suite.name, section); // Convert to lowercase std::transform(origin.begin(), origin.end(), origin.begin(), ::tolower); // Get current time in ISO8601 UTC auto now = std::chrono::floor(std::chrono::system_clock::now()); std::string timeNowIso8601 = std::format("{:%FT%TZ}", now); std::string mediaPoolUrl = fs::path(m_conf->mediaBaseUrl) / "pool"; if (m_conf->feature.immutableSuites) mediaPoolUrl = std::format("{}/{}", m_conf->mediaBaseUrl, suite.name); const bool mediaBaseUrlAllowed = !m_conf->mediaBaseUrl.empty() && m_conf->feature.storeScreenshots; if (m_conf->metadataType == DataType::XML) { head = "\n"; head += std::format("formatVersionStr(), origin); if (suite.dataPriority != 0) head += std::format(" priority=\"{}\"", suite.dataPriority); if (mediaBaseUrlAllowed) head += std::format(" media_baseurl=\"{}\"", mediaPoolUrl); if (m_conf->feature.metadataTimestamps) head += std::format(" time=\"{}\"", timeNowIso8601); head += ">"; } else { head = "%YAML 1.2\n---\n"; head += std::format( "File: DEP-11\n" "Version: '{}'\n" "Origin: {}", m_conf->formatVersionStr(), origin); if (mediaBaseUrlAllowed) head += std::format("\nMediaBaseUrl: {}", mediaPoolUrl); if (suite.dataPriority != 0) head += std::format("\nPriority: {}", suite.dataPriority); if (m_conf->feature.metadataTimestamps) head += std::format("\nTime: '{}'", timeNowIso8601); } return head; } void Engine::exportMetadata( const Suite &suite, const std::string §ion, const std::string &arch, const std::vector> &pkgs) { std::ostringstream mdataFile; std::ostringstream hintsFile; // Reserve some space for our data mdataFile.str().reserve(pkgs.size() / 2); hintsFile.str().reserve(512); // Prepare hints file hintsFile << "[\n"; logInfo("Exporting data for {} ({}/{})", suite.name, section, arch); // Add metadata document header mdataFile << getMetadataHead(suite, section) << "\n"; // Prepare destination const auto dataExportDir = m_conf->dataExportDir / suite.name / section; const auto hintsExportDir = m_conf->hintsExportDir / suite.name / section; fs::create_directories(dataExportDir); fs::create_directories(hintsExportDir); const bool useImmutableSuites = m_conf->feature.immutableSuites; // Select the media export target directory fs::path mediaExportDir; if (useImmutableSuites) mediaExportDir = m_dstore->mediaExportPoolDir().parent_path() / suite.name; else mediaExportDir = m_dstore->mediaExportPoolDir(); // Collect metadata, icons and hints for the given packages std::unordered_map cidGcidMap; bool firstHintEntry = true; std::mutex exportMutex; logDebug("Building final metadata and hints files."); tbb::parallel_for_each(pkgs.begin(), pkgs.end(), [&](std::shared_ptr pkg) { const auto &pkid = pkg->id(); auto gcids = m_dstore->getGCIDsForPackage(pkid); if (!gcids.empty()) { auto mres = m_dstore->getMetadataForPackage(m_conf->metadataType, pkid); if (!mres.empty()) { std::lock_guard lock(exportMutex); for (const auto &md : mres) mdataFile << md << "\n"; } for (const auto &gcid : gcids) { { std::lock_guard lock(exportMutex); const auto cid = Utils::getCidFromGlobalID(gcid); if (cid.has_value()) cidGcidMap[cid.value()] = gcid; else logError("Could not extract component-ID from GCID: {}", gcid); } // Hardlink data from the pool to the suite-specific directories if (useImmutableSuites) { const auto gcidMediaPoolPath = m_dstore->mediaExportPoolDir() / gcid; const auto gcidMediaSuitePath = mediaExportDir / gcid; if (!fs::exists(gcidMediaSuitePath) && fs::exists(gcidMediaPoolPath)) Utils::copyDir(gcidMediaPoolPath.string(), gcidMediaSuitePath.string(), true); } } } const auto hres = m_dstore->getHints(pkid); if (!hres.empty()) { std::lock_guard lock(exportMutex); if (firstHintEntry) { firstHintEntry = false; hintsFile << Utils::rtrimString(hres); } else { hintsFile << ",\n" << Utils::rtrimString(hres); } } }); fs::path dataBaseFname; if (m_conf->metadataType == DataType::XML) dataBaseFname = dataExportDir / std::format("Components-{}.xml", arch); else dataBaseFname = dataExportDir / std::format("Components-{}.yml", arch); const auto cidIndexFname = dataExportDir / std::format("CID-Index-{}.json", arch); const auto hintsBaseFname = hintsExportDir / std::format("Hints-{}.json", arch); // Write metadata logInfo("Writing metadata for {}/{} [{}]", suite.name, section, arch); // Add the closing XML tag for XML metadata if (m_conf->metadataType == DataType::XML) mdataFile << "\n"; // Compress metadata and save it to disk auto mdataFileStr = mdataFile.str(); std::vector mdataFileBytes(mdataFileStr.begin(), mdataFileStr.end()); compressAndSave(mdataFileBytes, dataBaseFname.string() + ".gz", ArchiveType::GZIP); compressAndSave(mdataFileBytes, dataBaseFname.string() + ".xz", ArchiveType::XZ); // Component ID index inja::json cidIndexJson = inja::json::object(); for (const auto &[cid, gcid] : cidGcidMap) cidIndexJson[cid] = gcid; auto cidIndexStr = cidIndexJson.dump(2); // Pretty print with 2-space indentation std::vector cidIndexData(cidIndexStr.begin(), cidIndexStr.end()); compressAndSave(cidIndexData, cidIndexFname.string() + ".gz", ArchiveType::GZIP); // Write hints logInfo("Writing hints for {}/{} [{}]", suite.name, section, arch); // Finalize the JSON hints document hintsFile << "\n]\n"; // Compress hints auto hintsFileStr = hintsFile.str(); std::vector hintsFileBytes(hintsFileStr.begin(), hintsFileStr.end()); compressAndSave(hintsFileBytes, hintsBaseFname.string() + ".gz", ArchiveType::GZIP); compressAndSave(hintsFileBytes, hintsBaseFname.string() + ".xz", ArchiveType::XZ); // Save a copy of the hints registry to be used by other tools // (this allows other apps to just resolve the hint tags to severities and explanations // without loading either AppStream or AppStream-Generator code) saveHintsRegistryToJsonFile((m_conf->hintsExportDir / suite.name / "hint-definitions.json").string()); } void Engine::exportIconTarballs( const Suite &suite, const std::string §ion, const std::vector> &pkgs) { // Determine data sources and destinations const auto dataExportDir = m_conf->dataExportDir / suite.name / section; fs::create_directories(dataExportDir); const bool useImmutableSuites = m_conf->feature.immutableSuites; const auto mediaExportDir = useImmutableSuites ? m_dstore->mediaExportPoolDir().parent_path() / suite.name : m_dstore->mediaExportPoolDir(); // Prepare icon-tarball array std::unordered_map> iconTarFiles; // Initialize icon policy iterator AscIconPolicyIter policyIter; asc_icon_policy_iter_init(&policyIter, m_conf->iconPolicy()); guint iconSizeInt; guint iconScale; AscIconState iconState; while (asc_icon_policy_iter_next(&policyIter, &iconSizeInt, &iconScale, &iconState)) { if (iconState == ASC_ICON_STATE_IGNORED || iconState == ASC_ICON_STATE_REMOTE_ONLY) continue; // We only want to create tarballs for cached icons const ImageSize iconSize(iconSizeInt, iconSizeInt, iconScale); iconTarFiles[iconSize.toString()] = std::vector(); iconTarFiles[iconSize.toString()].reserve(256); } logInfo("Creating icon tarballs for: {}/{}", suite.name, section); std::unordered_set processedDirs; std::mutex dirMutex; std::mutex iconMutex; tbb::parallel_for_each(pkgs.begin(), pkgs.end(), [&](std::shared_ptr pkg) { const auto &pkid = pkg->id(); auto gcids = m_dstore->getGCIDsForPackage(pkid); if (gcids.empty()) return; for (const auto &gcid : gcids) { // Compile list of icon-tarball files AscIconPolicyIter ipIter; asc_icon_policy_iter_init(&ipIter, m_conf->iconPolicy()); while (asc_icon_policy_iter_next(&ipIter, &iconSizeInt, &iconScale, &iconState)) { if (iconState == ASC_ICON_STATE_IGNORED || iconState == ASC_ICON_STATE_REMOTE_ONLY) continue; // Only add icon to cache tarball if we want a cache for the particular size const ImageSize iconSize(iconSizeInt, iconSizeInt, iconScale); const auto iconDir = mediaExportDir / gcid / "icons" / iconSize.toString(); // Skip adding icon entries if we've already investigated this directory { std::lock_guard lock(dirMutex); if (processedDirs.contains(iconDir.string())) continue; else processedDirs.insert(iconDir.string()); } if (!fs::exists(iconDir)) continue; for (const auto &entry : fs::directory_iterator(iconDir)) { if (entry.is_regular_file()) { std::lock_guard lock(iconMutex); iconTarFiles[iconSize.toString()].push_back(entry.path().string()); } } } } }); // Create the icon tarballs asc_icon_policy_iter_init(&policyIter, m_conf->iconPolicy()); while (asc_icon_policy_iter_next(&policyIter, &iconSizeInt, &iconScale, &iconState)) { if (iconState == ASC_ICON_STATE_IGNORED || iconState == ASC_ICON_STATE_REMOTE_ONLY) continue; const ImageSize iconSize(iconSizeInt, iconSizeInt, iconScale); auto iconTar = std::make_unique(ArchiveType::GZIP); iconTar->open((dataExportDir / std::format("icons-{}.tar.gz", iconSize.toString())).string()); auto &iconFiles = iconTarFiles[iconSize.toString()]; std::sort(iconFiles.begin(), iconFiles.end()); for (const auto &fname : iconFiles) iconTar->addFile(fname); iconTar->close(); } logInfo("Icon tarballs built for: {}/{}", suite.name, section); } std::unordered_map> Engine::getIconCandidatePackages( const Suite &suite, const std::string §ion, const std::string &arch) { // Always load the "main" and "universe" components, which contain most of the icon data // on Debian and Ubuntu. Load the "core" and "extra" components for Arch Linux. // FIXME: This is a hack, find a sane way to get rid of this, or at least get rid of the // distro-specific hardcoding. std::vector> pkgs; for (const auto &newSection : std::vector{"main", "universe", "core", "extra"}) { if (section != newSection && std::ranges::find(suite.sections, newSection) != suite.sections.end()) { auto sectionPkgs = m_pkgIndex->packagesFor(suite.name, newSection, arch); pkgs.insert(pkgs.end(), sectionPkgs.begin(), sectionPkgs.end()); if (!suite.baseSuite.empty()) { auto basePkgs = m_pkgIndex->packagesFor(suite.baseSuite, newSection, arch); pkgs.insert(pkgs.end(), basePkgs.begin(), basePkgs.end()); } } } if (!suite.baseSuite.empty()) { auto basePkgs = m_pkgIndex->packagesFor(suite.baseSuite, section, arch); pkgs.insert(pkgs.end(), basePkgs.begin(), basePkgs.end()); } auto sectionPkgs = m_pkgIndex->packagesFor(suite.name, section, arch); pkgs.insert(pkgs.end(), sectionPkgs.begin(), sectionPkgs.end()); std::unordered_map> pkgMap; for (auto &pkg : pkgs) { const auto &pkid = pkg->id(); pkgMap[pkid] = pkg; } return pkgMap; } std::shared_ptr Engine::processExtraMetainfoData( const Suite &suite, std::shared_ptr iconh, const std::string §ion, const std::string &arch, std::shared_ptr injMods) { if (suite.extraMetainfoDir.empty() && !injMods->hasRemovedComponents()) return nullptr; const auto extraMIDir = suite.extraMetainfoDir / section; const auto archExtraMIDir = extraMIDir / arch; if (suite.extraMetainfoDir.empty()) logInfo("Injecting component removal requests for {}/{}/{}", suite.name, section, arch); else logInfo("Loading additional metainfo from local directory for {}/{}/{}", suite.name, section, arch); // We create a dummy package to hold information for the injected components auto diPkg = std::make_shared(EXTRA_METAINFO_FAKE_PKGNAME, arch); diPkg->setDataLocation(extraMIDir.string()); diPkg->setArchDataLocation(archExtraMIDir.string()); diPkg->setMaintainer("AppStream Generator Maintainer"); // Ensure we have no leftover hints in the database. // Since this package never changes its version number, cruft data will not be automatically // removed for it. m_dstore->removePackage(diPkg->id()); // Analyze our dummy package just like all other packages auto mde = std::make_unique(m_dstore, iconh, nullptr, nullptr); auto gres = mde->processPackage(diPkg); // Add removal requests, as we can remove packages from frozen suites via overlays injMods->addRemovalRequestsToResult(&gres); // Write resulting data into the database m_dstore->addGeneratorResult(m_conf->metadataType, gres, true); return diPkg; } bool Engine::processSuiteSection(const Suite &suite, const std::string §ion, std::shared_ptr rgen) { auto reportgen = std::move(rgen); if (!reportgen) reportgen = std::make_shared(m_dstore.get()); // Load repo-level modifications auto injMods = std::make_shared(); try { injMods->loadForSuite(std::make_shared(suite)); } catch (const std::exception &e) { throw std::runtime_error( std::format("Unable to read modifications.json for suite {}: {}", suite.name, e.what())); } // Process packages by architecture std::vector> sectionPkgs; bool suiteDataChanged = false; for (const auto &arch : suite.architectures) { // Update package contents information and flag boring packages as ignored const bool foundInteresting = seedContentsData(suite, section, arch) || m_forced; // Check if the suite/section/arch has actually changed if (!foundInteresting) { logInfo("Skipping {}/{} [{}], no interesting new packages since last update.", suite.name, section, arch); continue; } // Process new packages auto pkgs = m_pkgIndex->packagesFor(suite.name, section, arch); auto iconh = std::make_shared( *m_cstore, m_dstore->mediaExportPoolDir(), getIconCandidatePackages(suite, section, arch), suite.iconTheme); processPackages(pkgs, iconh, injMods); // Read injected data and add it to the database as a fake package auto fakePkg = processExtraMetainfoData(suite, std::move(iconh), section, arch, injMods); if (fakePkg) pkgs.push_back(std::move(fakePkg)); // Export package data exportMetadata(suite, section, arch, pkgs); suiteDataChanged = true; // We store the package info over all architectures to generate reports later sectionPkgs.reserve(sectionPkgs.capacity() + pkgs.size()); sectionPkgs.insert(sectionPkgs.end(), pkgs.begin(), pkgs.end()); // Log progress logInfo("Completed metadata processing of {}/{} [{}]", suite.name, section, arch); } // Finalize if (suiteDataChanged) { // Export icons for the found packages in this section exportIconTarballs(suite, section, sectionPkgs); // Write reports & statistics and render HTML, if that option is selected reportgen->processFor(suite.name, section, sectionPkgs); } // Release the index to free some memory m_pkgIndex->release(); return suiteDataChanged; } SuiteUsabilityResult Engine::checkSuiteUsable(const std::string &suiteName) { SuiteUsabilityResult res; res.suiteUsable = false; bool suiteFound = false; for (const auto &s : m_conf->suites) { if (s.name == suiteName) { res.suite = s; suiteFound = true; break; } } if (!suiteFound) { logError("Suite '{}' was not found.", suiteName); return res; } if (res.suite.isImmutable) { // We also can't process anything if there are no architectures defined logError("Suite '{}' is marked as immutable. No changes are allowed.", res.suite.name); return res; } if (res.suite.sections.empty()) { // If we have no sections, we can't do anything but exit... logError("Suite '{}' has no sections. Can not continue.", res.suite.name); return res; } if (res.suite.architectures.empty()) { // We also can't process anything if there are no architectures defined logError("Suite '{}' has no architectures defined. Can not continue.", res.suite.name); return res; } // If we are here, we can process this suite res.suiteUsable = true; return res; } bool Engine::processFile( const std::string &suiteName, const std::string §ionName, const std::vector &files) { // Fetch suite and exit in case we can't write to it. auto suiteTuple = checkSuiteUsable(suiteName); if (!suiteTuple.suiteUsable) return false; auto suite = suiteTuple.suite; bool sectionValid = false; for (const auto §ion : suite.sections) { if (section == sectionName) { sectionValid = true; break; } } if (!sectionValid) { logError("Section '{}' does not exist in suite '{}'. Can not continue.", sectionName, suite.name); return false; } // ensure our libfyaml version is new enough checkLibfyamlVersion(); std::unordered_map>> pkgByArch; for (const auto &fname : files) { auto pkg = m_pkgIndex->packageForFile(fname, suiteName, sectionName); if (!pkg) { logError( "Could not get package representation for file '{}' from backend '{}': The backend might not support " "this feature.", fname, m_conf->backendName); return false; } pkgByArch[pkg->arch()].push_back(pkg); } for (const auto &[arch, pkgs] : pkgByArch) { // Update package contents information and flag boring packages as ignored const bool foundInteresting = seedContentsData(suite, sectionName, arch, pkgs); // Skip if the new package files have no interesting data if (!foundInteresting) { logInfo("Skipping {}/{} [{}], no interesting new packages.", suite.name, sectionName, arch); continue; } // Process new packages auto iconh = std::make_shared( *m_cstore, m_dstore->mediaExportPoolDir(), getIconCandidatePackages(suite, sectionName, arch), suite.iconTheme); processPackages(pkgs, std::move(iconh), nullptr); } return true; } void Engine::run() { logVersionInfo(); checkLibfyamlVersion(); bool dataChanged = false; auto reportgen = std::make_shared(m_dstore.get()); for (const auto &suite : m_conf->suites) { if (suite.isImmutable) { logDebug("Skipping immutable suite: {}", suite.name); continue; } // The `checkSuiteUsable` method will print an error // message in case the suite isn't usable. auto suiteCheck = checkSuiteUsable(suite.name); if (!suiteCheck.suiteUsable) continue; printHeaderBox(std::format("Processing: {}", suite.name)); // process the suite for (const auto §ion : suite.sections) { printSectionBox(std::format("{}: {}", suite.name, section)); const bool ret = processSuiteSection(suite, section, reportgen); if (ret) dataChanged = true; } } // Render index pages & statistics printHeaderBox("Updating Global Data"); reportgen->updateIndexPages(); if (dataChanged) reportgen->exportStatistics(); } void Engine::run(const std::string &suiteName) { // Fetch suite and exit in case we can't write to it. // The `checkSuiteUsable` method will print an error // message in case the suite isn't usable. auto suiteCheck = checkSuiteUsable(suiteName); if (!suiteCheck.suiteUsable) return; auto suite = suiteCheck.suite; logVersionInfo(); checkLibfyamlVersion(); auto reportgen = std::make_shared(m_dstore.get()); bool dataChanged = false; for (const auto §ion : suite.sections) { const bool ret = processSuiteSection(suite, section, reportgen); if (ret) dataChanged = true; } // Render index pages & statistics reportgen->updateIndexPages(); if (dataChanged) reportgen->exportStatistics(); } void Engine::run(const std::string &suiteName, const std::string §ionName) { // Fetch suite and exit in case we can't write to it. // The `checkSuiteUsable` method will print an error // message in case the suite isn't usable. auto suiteCheck = checkSuiteUsable(suiteName); if (!suiteCheck.suiteUsable) return; auto suite = suiteCheck.suite; logVersionInfo(); checkLibfyamlVersion(); bool sectionValid = false; for (const auto §ion : suite.sections) { if (section == sectionName) { sectionValid = true; break; } } if (!sectionValid) { logError("Section '{}' does not exist in suite '{}'. Can not continue.", sectionName, suite.name); return; } auto reportgen = std::make_shared(m_dstore.get()); auto dataChanged = processSuiteSection(suite, sectionName, reportgen); // Render index pages & statistics reportgen->updateIndexPages(); if (dataChanged) reportgen->exportStatistics(); } void Engine::publishMetadataForSuiteSection( const Suite &suite, const std::string §ion, std::shared_ptr rgen) { auto reportgen = std::move(rgen); if (!reportgen) reportgen = std::make_shared(m_dstore.get()); std::vector> sectionPkgs; for (const auto &arch : suite.architectures) { // Process new packages auto pkgs = m_pkgIndex->packagesFor(suite.name, section, arch); // Export package data exportMetadata(suite, section, arch, pkgs); // We store the package info over all architectures to generate reports later sectionPkgs.insert(sectionPkgs.end(), pkgs.begin(), pkgs.end()); // Log progress logInfo("Completed publishing of data for {}/{} [{}]", suite.name, section, arch); } // Export icons for the found packages in this section exportIconTarballs(suite, section, sectionPkgs); // Write reports & statistics and render HTML, if that option is selected reportgen->processFor(suite.name, section, sectionPkgs); // Free some memory explicitly m_pkgIndex->release(); } void Engine::publish(const std::string &suiteName) { // Fetch suite and exit in case we can't write to it. auto scResult = checkSuiteUsable(suiteName); if (!scResult.suiteUsable) return; auto suite = scResult.suite; logVersionInfo(); auto reportgen = std::make_shared(m_dstore.get()); for (const auto §ion : suite.sections) publishMetadataForSuiteSection(suite, section, reportgen); // Render index pages & statistics reportgen->updateIndexPages(); reportgen->exportStatistics(); } void Engine::publish(const std::string &suiteName, const std::string §ionName) { // Fetch suite and exit in case we can't write to it. auto scResult = checkSuiteUsable(suiteName); if (!scResult.suiteUsable) return; auto suite = scResult.suite; logVersionInfo(); bool sectionValid = false; for (const auto §ion : suite.sections) { if (section == sectionName) { sectionValid = true; break; } } if (!sectionValid) { logError("Section '{}' does not exist in suite '{}'. Can not continue.", sectionName, suite.name); return; } auto reportgen = std::make_shared(m_dstore.get()); publishMetadataForSuiteSection(suite, sectionName, reportgen); // Render index pages & statistics reportgen->updateIndexPages(); reportgen->exportStatistics(); } void Engine::cleanupStatistics() { auto allStats = m_dstore->getStatistics(); std::sort(allStats.begin(), allStats.end(), [](const auto &a, const auto &b) { return a.time < b.time; }); std::unordered_map> lastStatData; std::unordered_map lastTime; for (const auto &entry : allStats) { // We don't clean up combined statistics entries, and therefore need to reset // the last-data hashmaps as soon as we encounter one to not lose data. auto suiteIt = entry.data.find("suite"); auto sectionIt = entry.data.find("section"); if (suiteIt == entry.data.end() || sectionIt == entry.data.end()) { lastStatData.clear(); lastTime.clear(); continue; } const auto ssid = std::format( "{}-{}", std::get(suiteIt->second), std::get(sectionIt->second)); if (lastStatData.find(ssid) == lastStatData.end()) { lastStatData[ssid] = entry.serialize(); lastTime[ssid] = entry.time; continue; } auto sdata = entry.serialize(); if (lastStatData[ssid] == sdata) { logInfo("Removing superfluous statistics entry: {}", lastTime[ssid]); m_dstore->removeStatistics(lastTime[ssid]); } lastTime[ssid] = entry.time; lastStatData[ssid] = std::move(sdata); } } void Engine::runCleanup() { logVersionInfo(); logInfo("Cleaning up left over temporary data."); const auto tmpDir = m_conf->cacheRootDir() / "tmp"; if (fs::exists(tmpDir)) fs::remove_all(tmpDir); logInfo("Collecting information."); // Get sets of all packages registered in the database std::unordered_set pkidsContents; std::unordered_set pkidsData; // Parallel collection of package IDs tbb::parallel_invoke( [&]() { pkidsContents = m_cstore->getPackageIdSet(); }, [&]() { pkidsData = m_dstore->getPackageIdSet(); }); logInfo("We have data on a total of {} packages (content lists on {})", pkidsData.size(), pkidsContents.size()); // Build a set of all valid packages for (const auto &suite : m_conf->suites) { if (suite.isImmutable) continue; // Data from immutable suites is ignored for (const auto §ion : suite.sections) { for (const auto &arch : suite.architectures) { // Fetch current packages without long descriptions, we really only are interested in the pkgid auto pkgs = m_pkgIndex->packagesFor(suite.name, section, arch, false); if (!suite.baseSuite.empty()) { auto basePkgs = m_pkgIndex->packagesFor(suite.baseSuite, section, arch, false); pkgs.insert(pkgs.end(), basePkgs.begin(), basePkgs.end()); } { std::lock_guard lock(m_mutex); for (const auto &pkg : pkgs) { // Remove packages from the sets that are still active pkidsContents.erase(pkg->id()); pkidsData.erase(pkg->id()); } // Free some memory m_pkgIndex->release(); } } } } // Release index resources m_pkgIndex->release(); logInfo("Cleaning up superseded data ({} hints/data, {} content lists).", pkidsData.size(), pkidsContents.size()); // Remove packages from the caches which are no longer in the archive tbb::parallel_invoke( [&]() { m_cstore->removePackages(pkidsContents); }, [&]() { m_dstore->removePackages(pkidsData); }); // Remove orphaned data and media logInfo("Cleaning up obsolete media."); m_dstore->cleanupCruft(); // Cleanup duplicate statistical entries logInfo("Cleaning up excess statistical data."); cleanupStatistics(); } void Engine::removeHintsComponents(const std::string &suiteName) { auto st = checkSuiteUsable(suiteName); if (!st.suiteUsable) return; auto suite = st.suite; logVersionInfo(); for (const auto §ion : suite.sections) { const auto &architectures = suite.architectures; tbb::parallel_for_each(architectures.begin(), architectures.end(), [&](const std::string &arch) { auto pkgs = m_pkgIndex->packagesFor(suite.name, section, arch, false); for (const auto &pkg : pkgs) { const auto &pkid = pkg->id(); if (!m_dstore->packageExists(pkid)) continue; if (m_dstore->isIgnored(pkid)) continue; m_dstore->removePackage(pkid); } }); m_pkgIndex->release(); } m_dstore->cleanupCruft(); m_pkgIndex->release(); } void Engine::forgetPackage(const std::string &identifier) { const auto slashCount = std::count(identifier.begin(), identifier.end(), '/'); std::unordered_set affectedGCIDs; if (slashCount == 2) { // We have a package-id, so we can do a targeted remove const auto pkid = identifier; logDebug("Considering {} to be a package-id.", pkid); const auto gcids = m_dstore->getGCIDsForPackage(pkid); affectedGCIDs.insert(gcids.begin(), gcids.end()); if (m_cstore->packageExists(pkid)) m_cstore->removePackage(pkid); if (m_dstore->packageExists(pkid)) m_dstore->removePackage(pkid); logInfo("Removed package with ID: {}", pkid); } else { auto pkids = m_dstore->getPkidsMatching(identifier); for (const auto &pkid : pkids) { const auto gcids = m_dstore->getGCIDsForPackage(pkid); affectedGCIDs.insert(gcids.begin(), gcids.end()); m_dstore->removePackage(pkid); if (m_cstore->packageExists(pkid)) m_cstore->removePackage(pkid); logInfo("Removed package with ID: {}", pkid); } } // Remove orphaned data and media m_dstore->cleanupCruft(); // Report if data is kept because packages keep the GCIDs around std::cout << "Packages still using the same data from the removed package(s):" << std::endl; auto pkgsForGCIDs = m_dstore->getPackagesForGCIDs(affectedGCIDs); if (pkgsForGCIDs.empty()) { std::cout << " - None" << std::endl; } else { for (const auto &it : pkgsForGCIDs) { std::cout << " - " << it.first << ": "; for (const auto &gcid : it.second) std::cout << gcid << " "; std::cout << std::endl; } } } bool Engine::printPackageInfo(const std::string &identifier) { const auto slashCount = std::count(identifier.begin(), identifier.end(), '/'); if (slashCount != 2) { std::cout << "Please enter a package-id in the format //\n"; return false; } const auto pkid = identifier; std::cout << "== " << pkid << " ==\n"; std::cout << "Contents:\n"; auto pkgContents = m_cstore->getContents(pkid); if (pkgContents.empty()) { std::cout << "~ No contents found.\n"; } else { for (const auto &s : pkgContents) std::cout << " " << s << "\n"; } std::cout << "\n"; std::cout << "Icons:\n"; auto pkgIcons = m_cstore->getIcons(pkid); if (pkgIcons.empty()) { std::cout << "~ No icons found.\n"; } else { for (const auto &s : pkgIcons) std::cout << " " << s << "\n"; } std::cout << "\n"; if (m_dstore->isIgnored(pkid)) { std::cout << "Ignored: yes\n"; std::cout << "\n"; } else { std::cout << "Global Component IDs:\n"; for (const auto &s : m_dstore->getGCIDsForPackage(pkid)) std::cout << "- " << s << "\n"; std::cout << "\n"; std::cout << "Generated Data:\n"; for (const auto &s : m_dstore->getMetadataForPackage(m_conf->metadataType, pkid)) std::cout << s << "\n"; std::cout << "\n"; } if (m_dstore->hasHints(pkid)) { std::cout << "Hints:\n"; std::cout << m_dstore->getHints(pkid) << "\n"; } else { std::cout << "Hints: None\n"; } std::cout << "\n"; return true; } } // namespace ASGenerator appstream-generator-0.10.1/src/engine.h000066400000000000000000000141051506754475600200020ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include "config.h" #include "datastore.h" #include "contentsstore.h" #include "backends/interfaces.h" #include "iconhandler.h" #include "reportgenerator.h" #include "cptmodifiers.h" namespace ASGenerator { /** * Structure to represent the result of checking a suite's usability. */ struct SuiteUsabilityResult { Suite suite; bool suiteUsable = false; }; /** * Class orchestrating the whole metadata extraction * and publication process. */ class Engine { public: Engine(); ~Engine() = default; // Delete copy constructor and assignment operator Engine(const Engine &) = delete; Engine &operator=(const Engine &) = delete; bool forced() const; void setForced(bool v); bool processFile( const std::string &suiteName, const std::string §ionName, const std::vector &files); /** * Run the metadata extractor on all suitable suites. */ void run(); /** * Run the metadata extractor on a suite and all of its sections. */ void run(const std::string &suiteName); /** * Run the metadata extractor on a single section of a suite. */ void run(const std::string &suiteName, const std::string §ionName); /** * Run the metadata publishing step only, for a suite and all of its sections. */ void publish(const std::string &suiteName); /** * Run the metadata publishing step only, on a single section of a suite. */ void publish(const std::string &suiteName, const std::string §ionName); void runCleanup(); /** * Drop all packages which contain valid components or hints * from the database. * This is useful when big generator changes have been done, which * require reprocessing of all components. */ void removeHintsComponents(const std::string &suiteName); void forgetPackage(const std::string &identifier); /** * Print all information we have on a package to stdout. */ bool printPackageInfo(const std::string &identifier); private: Config *m_conf; std::unique_ptr m_pkgIndex; std::shared_ptr m_dstore; std::shared_ptr m_cstore; bool m_forced; std::unique_ptr m_taskArena; mutable std::mutex m_mutex; void logVersionInfo(); /** * Throw an error if the libfyaml version is bad. */ void checkLibfyamlVersion(); /** * Extract metadata from a software container (usually a distro package). * The result is automatically stored in the database. */ void processPackages( const std::vector> &pkgs, std::shared_ptr iconh, std::shared_ptr injMods); /** * Populate the contents index with new contents data. While we are at it, we can also mark * some uninteresting packages as to-be-ignored, so we don't waste time on them * during the following metadata extraction. * * Returns: True in case we have new interesting packages, false otherwise. */ bool seedContentsData( const Suite &suite, const std::string §ion, const std::string &arch, const std::vector> &pkgs = {}); std::string getMetadataHead(const Suite &suite, const std::string §ion); /** * Export metadata and issue hints from the database and store them as files. */ void exportMetadata( const Suite &suite, const std::string §ion, const std::string &arch, const std::vector> &pkgs); /** * Export all icons for the given set of packages and publish them in the selected suite/section. * Package icon duplicates will be eliminated automatically. */ void exportIconTarballs( const Suite &suite, const std::string §ion, const std::vector> &pkgs); std::unordered_map> getIconCandidatePackages( const Suite &suite, const std::string §ion, const std::string &arch); /** * Read metainfo and auxiliary data injected by the person running the data generator. */ std::shared_ptr processExtraMetainfoData( const Suite &suite, std::shared_ptr iconh, const std::string §ion, const std::string &arch, std::shared_ptr injMods); /** * Scan and export data and hints for a specific section in a suite. */ bool processSuiteSection(const Suite &suite, const std::string §ion, std::shared_ptr rgen); /** * Fetch a suite definition from a suite name and test whether we can process it. */ SuiteUsabilityResult checkSuiteUsable(const std::string &suiteName); /** * Export data and hints for a specific section in a suite. */ void publishMetadataForSuiteSection( const Suite &suite, const std::string §ion, std::shared_ptr rgen); void cleanupStatistics(); }; } // namespace ASGenerator appstream-generator-0.10.1/src/extractor.cpp000066400000000000000000000411611506754475600211050ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "extractor.h" #include #include #include #include #include #include #include #include "config.h" #include "logging.h" #include "hintregistry.h" #include "result.h" #include "backends/interfaces.h" #include "datastore.h" #include "iconhandler.h" #include "utils.h" #include "cptmodifiers.h" namespace ASGenerator { DataExtractor::DataExtractor( std::shared_ptr db, std::shared_ptr iconHandler, AsgLocaleUnit *localeUnit, std::shared_ptr modInjInfo) : m_compose(nullptr), m_dstore(std::move(db)), m_iconh(std::move(iconHandler)), m_modInj(std::move(modInjInfo)), m_l10nUnit(nullptr) { m_conf = &Config::get(); m_dtype = m_conf->metadataType; if (localeUnit != nullptr) m_l10nUnit = g_object_ref(localeUnit); m_compose = asc_compose_new(); asc_compose_set_media_result_dir(m_compose, m_dstore->mediaExportPoolDir().string().c_str()); asc_compose_set_media_baseurl(m_compose, ""); // Set callback for intermediate metadata checking asc_compose_set_check_metadata_early_func(m_compose, &checkMetadataIntermediate, this); AscComposeFlags flags = static_cast( ASC_COMPOSE_FLAG_IGNORE_ICONS | // we do custom icon processing ASC_COMPOSE_FLAG_PROCESS_UNPAIRED_DESKTOP | // handle desktop-entry files without metainfo data ASC_COMPOSE_FLAG_NO_FINAL_CHECK); // we trigger the final check manually asc_compose_add_flags(m_compose, flags); // we handle all threading, so the compose process doesn't also have to be threaded asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_USE_THREADS); // set CAInfo for any download operations performed by this AscCompose if (!m_conf->caInfo.empty()) asc_compose_set_cainfo(m_compose, m_conf->caInfo.c_str()); // set dummy locale unit for advanced locale processing if (m_l10nUnit) asc_compose_set_locale_unit(m_compose, ASC_UNIT(m_l10nUnit)); // set max screenshot size in bytes, if size is limited if (m_conf->maxScrFileSize != 0) { auto maxSize = static_cast(m_conf->maxScrFileSize * 1024 * 1024); asc_compose_set_max_screenshot_size(m_compose, maxSize); } // enable or disable user-defined features if (m_conf->feature.validate) asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_VALIDATE); else asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_VALIDATE); if (m_conf->feature.noDownloads) asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_ALLOW_NET); else asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_ALLOW_NET); if (m_conf->feature.processLocale) asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_PROCESS_TRANSLATIONS); else asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_PROCESS_TRANSLATIONS); if (m_conf->feature.processFonts) asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_PROCESS_FONTS); else asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_PROCESS_FONTS); if (m_conf->feature.storeScreenshots) asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS); else asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS); if (m_conf->feature.screenshotVideos) asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_ALLOW_SCREENCASTS); else asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_ALLOW_SCREENCASTS); if (m_conf->feature.propagateMetaInfoArtifacts) asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_PROPAGATE_ARTIFACTS); else asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_PROPAGATE_ARTIFACTS); // override icon policy with our own, possible user-modified one asc_compose_set_icon_policy(m_compose, m_conf->iconPolicy()); // register allowed custom keys with the composer if (!m_conf->allowedCustomKeys.empty()) { asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_PROPAGATE_CUSTOM); for (const auto &[key, _] : m_conf->allowedCustomKeys) asc_compose_add_custom_allowed(m_compose, key.c_str()); } else { asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_PROPAGATE_CUSTOM); } } DataExtractor::~DataExtractor() { g_object_unref(m_compose); if (m_l10nUnit) g_object_unref(m_l10nUnit); } void DataExtractor::checkMetadataIntermediate(AscResult *cres, const AscUnit *cunit, void *userData) { auto self = static_cast(userData); auto cptsPtrArray = asc_result_fetch_components(cres); for (guint i = 0; i < cptsPtrArray->len; i++) { auto cpt = AS_COMPONENT(g_ptr_array_index(cptsPtrArray, i)); auto gcid = asc_result_gcid_for_component(cres, cpt); if (gcid == nullptr) { logWarning( "Component {} has no global ID, skipping intermediate metadata check.", as_component_get_id(cpt)); continue; } // don't run expensive operations later if the metadata already exists auto existingMData = self->m_dstore->getMetadata(self->m_dtype, gcid); // skip if no existing metadata is present if (existingMData.empty()) continue; const auto bundleId = asc_result_get_bundle_id(cres); if (bundleId && std::string(bundleId) == EXTRA_METAINFO_FAKE_PKGNAME) { // the "package" was injected and therefore has likely already been unlinked // and we will want to reprocess it unconditionally. Therefore, we just skip // all following checks on same-package and duplicate IDs and just continue // processing the metadata without modifications. continue; } // To account for packages which change their package name, we // also need to check if the package this component is associated // with matches ours. // If it doesn't, we can't just link the package to the component. bool samePkg = false; if (self->m_dtype == DataType::YAML) { if (existingMData.find(std::format("Package: {}\n", bundleId)) != std::string::npos) samePkg = true; } else { if (existingMData.find(std::format("{}", bundleId)) != std::string::npos) samePkg = true; } if ((!samePkg) && (as_component_get_kind(cpt) != AS_COMPONENT_KIND_WEB_APP)) { // The exact same metadata exists in a different package already, we emit an error hint. // ATTENTION: This does not cover the case where *different* metadata (as in, different summary etc.) // but with the *same ID* exists. // We only catch that kind of problem later. g_autoptr(AsMetadata) cdata = as_metadata_new(); as_metadata_set_format_style(cdata, AS_FORMAT_STYLE_CATALOG); as_metadata_set_format_version(cdata, self->m_conf->formatVersion); g_autoptr(GError) error = nullptr; if (self->m_dtype == DataType::YAML) as_metadata_parse_data(cdata, existingMData.c_str(), -1, AS_FORMAT_KIND_YAML, &error); else as_metadata_parse_data(cdata, existingMData.c_str(), -1, AS_FORMAT_KIND_XML, &error); if (error) throw std::runtime_error( std::format("Failed to parse existing metadata for duplicate check: {}", error->message)); auto ecpt = as_metadata_get_component(cdata); if (!ecpt) continue; auto pkgNames = as_component_get_pkgnames(ecpt); std::string pkgName = "(none)"; if (pkgNames && pkgNames[0]) pkgName = pkgNames[0]; asc_result_add_hint( cres, cpt, "metainfo-duplicate-id", "cid", as_component_get_id(cpt), "pkgname", pkgName.c_str(), nullptr); } // drop the component as we already have processed it, but keep its // global ID so we can still register the ID with this package. asc_result_remove_component_full(cres, cpt, FALSE); } } GPtrArray *DataExtractor::translateDesktopTextCallback(GKeyFile *dePtr, const char *text, void *userData) { auto pkg = *static_cast(userData); auto res = g_ptr_array_new_with_free_func(g_free); auto translations = pkg->getDesktopFileTranslations(dePtr, std::string(text)); for (const auto &[key, value] : translations) { g_ptr_array_add(res, g_strdup(key.c_str())); g_ptr_array_add(res, g_strdup(value.c_str())); } return res; } GeneratorResult DataExtractor::processPackage(std::shared_ptr pkg) { // reset compose instance to clear data from any previous invocation asc_compose_reset(m_compose); // set external desktop-entry translation function, if needed const bool externalL10n = pkg->hasDesktopFileTranslations(); Package *pkgPtr = pkg.get(); asc_compose_set_desktop_entry_l10n_func( m_compose, externalL10n ? &translateDesktopTextCallback : nullptr, externalL10n ? &pkgPtr : nullptr); // wrap package into unit, so AppStream Compose can work with it auto unit = asg_package_unit_new(pkg); asc_compose_add_unit(m_compose, ASC_UNIT(unit)); // process all data g_autoptr(GError) error = nullptr; if (!asc_compose_run(m_compose, nullptr, &error)) throw std::runtime_error( std::format("Failed to run compose process: {}", error ? error->message : "Unknown error")); auto resultsArray = asc_compose_get_results(m_compose); // we processed one unit, so should always generate one result if (resultsArray->len != 1) throw std::runtime_error( std::format("Expected 1 result for data extraction, but retrieved {}.", resultsArray->len)); // create result wrapper GeneratorResult gres(ASC_RESULT(g_ptr_array_index(resultsArray, 0)), pkg); // process icons and perform additional refinements g_autoptr(GPtrArray) cptsPtrArray = gres.fetchComponents(); for (guint i = 0; i < cptsPtrArray->len; i++) { auto cpt = AS_COMPONENT(g_ptr_array_index(cptsPtrArray, i)); const auto ckind = as_component_get_kind(cpt); auto context = as_component_get_context(cpt); if (!context) { context = as_context_new(); as_component_set_context(cpt, context); } // find & store icons m_iconh->process(gres, cpt); if (gres.isIgnored(cpt)) continue; // add fallback long descriptions only for desktop apps, console apps and web apps if (as_component_get_merge_kind(cpt) != AS_MERGE_KIND_NONE) continue; if (ckind != AS_COMPONENT_KIND_DESKTOP_APP && ckind != AS_COMPONENT_KIND_CONSOLE_APP && ckind != AS_COMPONENT_KIND_WEB_APP) continue; // inject package descriptions, if needed auto valueFlags = as_context_get_value_flags(context); valueFlags = static_cast(valueFlags | AS_VALUE_FLAG_NO_TRANSLATION_FALLBACK); as_context_set_value_flags(context, valueFlags); as_context_set_locale(context, "C"); const auto cptDesc = as_component_get_description(cpt); if (cptDesc != nullptr && cptDesc[0] != '\0') continue; // component doesn't have a long description, add one from the packaging. bool desc_added = false; for (const auto &[lang, desc] : pkg->description()) { as_component_set_description(cpt, desc.c_str(), lang.c_str()); desc_added = true; } if (desc_added) { // we only add the "description-from-package" tag if we haven't already // emitted a "no-metainfo" tag, to avoid two hints explaining the same thing if (!gres.hasHint(cpt, "no-metainfo")) { if (!gres.addHint(cpt, "description-from-package")) continue; } } else { std::string kindStr = as_component_kind_to_string(ckind); if (!gres.addHint( cpt, "description-missing", { {"kind", kindStr} })) continue; } } // handle GStreamer integration (usually for Ubuntu) if (m_conf->feature.processGStreamer && pkg->gst().has_value() && pkg->gst()->isNotEmpty()) { std::ostringstream data; data.str().reserve(200); g_autoptr(AsComponent) cpt = as_component_new(); as_component_set_id(cpt, pkg->name().c_str()); as_component_set_kind(cpt, AS_COMPONENT_KIND_CODEC); as_component_set_name(cpt, "GStreamer Multimedia Codecs", "C"); for (const auto &[lang, desc] : pkg->summary()) { as_component_set_summary(cpt, desc.c_str(), lang.c_str()); data << desc; } gres.addComponentWithString(cpt, data.str()); } // perform final checks asc_compose_finalize_results(m_compose); // do our own final validation g_ptr_array_unref(g_steal_pointer(&cptsPtrArray)); cptsPtrArray = gres.fetchComponents(); for (guint i = 0; i < cptsPtrArray->len; i++) { auto cpt = AS_COMPONENT(g_ptr_array_index(cptsPtrArray, i)); const auto ckind = as_component_get_kind(cpt); const auto cid = as_component_get_id(cpt); if (m_modInj) { // drop component that the repository owner wants to remove if (m_modInj->isComponentRemoved(cid)) { gres.removeComponent(cpt); continue; } // inject custom fields from the repository owner, if we have any auto injectedCustom = m_modInj->injectedCustomData(cid); if (injectedCustom.has_value()) { for (const auto &[key, value] : injectedCustom.value()) as_component_insert_custom_value(cpt, key.c_str(), value.c_str()); } } if (as_component_get_merge_kind(cpt) != AS_MERGE_KIND_NONE) continue; auto pkgnames = as_component_get_pkgnames(cpt); if (!pkgnames || !pkgnames[0]) { // no packages are associated with this component if (ckind != AS_COMPONENT_KIND_WEB_APP && ckind != AS_COMPONENT_KIND_OPERATING_SYSTEM && ckind != AS_COMPONENT_KIND_REPOSITORY) { // this component is not allowed to have no installation candidate if (!as_component_has_bundle(cpt)) { if (!gres.addHint(cpt, "no-install-candidate")) continue; } } } else { // packages are associated with this component if (pkg->kind() == PackageKind::Fake) { // drop any association with the dummy package std::vector filteredPkgnames; for (int j = 0; pkgnames[j] != nullptr; j++) { if (std::string(pkgnames[j]) != EXTRA_METAINFO_FAKE_PKGNAME) filteredPkgnames.push_back(pkgnames[j]); } // We only keep the C++ vector for memory management purposes, // as calling as_component_set_pkgnames() will free the originally // referenced data. g_autoptr(GStrvBuilder) builder = g_strv_builder_new(); for (const auto &pkgname : filteredPkgnames) g_strv_builder_add(builder, pkgname.c_str()); g_auto(GStrv) filteredArray = g_strv_builder_end(builder); as_component_set_pkgnames(cpt, filteredArray); } } } // clean up and return result pkg->finish(); return gres; } } // namespace ASGenerator appstream-generator-0.10.1/src/extractor.h000066400000000000000000000051041506754475600205470ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include "config.h" #include "datastore.h" #include "iconhandler.h" #include "result.h" #include "backends/interfaces.h" #include "cptmodifiers.h" #include "dataunits.h" namespace ASGenerator { /** * Class for extracting AppStream metadata from packages. */ class DataExtractor { public: DataExtractor( std::shared_ptr db, std::shared_ptr iconHandler, AsgLocaleUnit *localeUnit, std::shared_ptr modInjInfo = nullptr); ~DataExtractor(); /** * Process a package and extract AppStream metadata from it. * * @param pkg The package to process * @return GeneratorResult containing the extraction results */ GeneratorResult processPackage(std::shared_ptr pkg); // Delete copy constructor and assignment operator DataExtractor(const DataExtractor &) = delete; DataExtractor &operator=(const DataExtractor &) = delete; private: Config *m_conf; AscCompose *m_compose; DataType m_dtype; std::shared_ptr m_dstore; std::shared_ptr m_iconh; std::shared_ptr m_modInj; AsgLocaleUnit *m_l10nUnit; /** * Helper function for early asgen-specific metadata manipulation. * This is a C callback function used by AppStream Compose. */ static void checkMetadataIntermediate(AscResult *cres, const AscUnit *cunit, void *userData); /** * Helper function for translating desktop file text. * This is a C callback function for desktop entry translation. */ static GPtrArray *translateDesktopTextCallback(GKeyFile *dePtr, const char *text, void *userData); }; } // namespace ASGenerator appstream-generator-0.10.1/src/hintregistry.cpp000066400000000000000000000205571506754475600216330ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "hintregistry.h" #include #include #include #include #include #include #include #include "logging.h" #include "utils.h" #include "yaml-utils.h" namespace ASGenerator { static std::mutex g_hintsRegistryMutex; void loadHintsRegistry() { std::lock_guard lock(g_hintsRegistryMutex); static bool registryLoaded = false; if (registryLoaded) { logDebug("Hints registry already loaded, ignoring second load request."); return; } // find the hint definition file auto hintsDefFile = Utils::getDataPath("asgen-hints.json"); if (!std::filesystem::exists(hintsDefFile)) { logError( "Hints definition file '{}' was not found! This means we can not determine severity of issue tags and not " "render report pages.", hintsDefFile.string()); return; } // read the hints definition JSON file std::ifstream file(hintsDefFile); if (!file.is_open()) { logError("Failed to open hints definition file '{}'", hintsDefFile.string()); return; } std::string jsonData((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); // Parse JSON auto doc = Yaml::parseDocument(jsonData, true); auto root = Yaml::documentRoot(doc); if (!root || fy_node_get_type(root) != FYNT_MAPPING) { logError("Invalid hints definition file format"); return; } bool checkAlreadyLoaded = true; // Iterate through all hint definitions fy_node_pair *pair; void *iter = nullptr; while ((pair = fy_node_mapping_iterate(root, &iter)) != nullptr) { fy_node *keyNode = fy_node_pair_key(pair); fy_node *valueNode = fy_node_pair_value(pair); if (!keyNode || !valueNode) continue; // Get tag name const auto tag = Yaml::nodeStrValue(keyNode); if (tag.empty()) continue; if (fy_node_get_type(valueNode) != FYNT_MAPPING) continue; // Get severity auto severityNode = fy_node_mapping_lookup_by_string(valueNode, "severity", FY_NT); if (!severityNode) continue; const auto severityStr = Yaml::nodeStrValue(severityNode); if (severityStr.empty()) continue; auto severity = as_issue_severity_from_string(severityStr.c_str()); // Get explanation text auto textNode = fy_node_mapping_lookup_by_string(valueNode, "text", FY_NT); if (!textNode) continue; std::string explanation; if (fy_node_get_type(textNode) == FYNT_SEQUENCE) { // Text is an array of lines fy_node *lineNode; void *textIter = nullptr; while ((lineNode = fy_node_sequence_iterate(textNode, &textIter)) != nullptr) { const auto line = Yaml::nodeStrValue(lineNode); if (!line.empty()) explanation += line + "\n"; } } else { // Text is a single string const auto text = Yaml::nodeStrValue(textNode); if (!text.empty()) explanation = text; } bool overrideExisting = false; if (tag == "icon-not-found" || tag == "internal-unknown-tag" || tag == "internal-error" || tag == "no-metainfo") { overrideExisting = true; } if (checkAlreadyLoaded) { // Check if hints are already loaded by looking for a common tag if (!overrideExisting && asc_globals_hint_tag_severity(tag.c_str()) != AS_ISSUE_SEVERITY_UNKNOWN) { logDebug("Global hints registry already loaded."); return; } checkAlreadyLoaded = false; } if (!asc_globals_add_hint_tag(tag.c_str(), severity, explanation.c_str(), overrideExisting)) logError("Unable to override existing hint tag: {}", tag); } registryLoaded = true; } void saveHintsRegistryToJsonFile(const std::string &fname) { std::lock_guard lock(g_hintsRegistryMutex); // Create YAML document for JSON output auto doc = Yaml::createDocument(); if (!doc) throw std::runtime_error("Failed to create document for hints registry export"); fy_node *root = fy_node_create_mapping(doc.get()); fy_document_set_root(doc.get(), root); g_auto(GStrv) hintTags = asc_globals_get_hint_tags(); for (guint i = 0; hintTags[i] != nullptr; i++) { const gchar *tag = hintTags[i]; const auto hdef = retrieveHintDef(tag); // Create mapping for this hint fy_node *hintMapping = fy_node_create_mapping(doc.get()); // Add text field fy_node *textKey = fy_node_create_scalar(doc.get(), "text", FY_NT); fy_node *textValue = fy_node_create_scalar_copy(doc.get(), hdef.explanation.c_str(), FY_NT); fy_node_mapping_append(hintMapping, textKey, textValue); // Add severity field fy_node *severityKey = fy_node_create_scalar(doc.get(), "severity", FY_NT); fy_node *severityValue = fy_node_create_scalar(doc.get(), as_issue_severity_to_string(hdef.severity), FY_NT); fy_node_mapping_append(hintMapping, severityKey, severityValue); // Add to root mapping fy_node *tagKey = fy_node_create_scalar(doc.get(), tag, FY_NT); fy_node_mapping_append(root, tagKey, hintMapping); } // Emit as JSON g_autofree char *json_output = fy_emit_document_to_string( doc.get(), static_cast(FYECF_MODE_JSON | FYECF_INDENT_DEFAULT)); if (json_output) { std::ofstream file(fname); if (file.is_open()) { file.write(json_output, strlen(json_output)); file.close(); } else { logError("Failed to open file '{}' for writing", fname); } } else { throw std::runtime_error("Failed to emit hints registry as JSON"); } } HintDefinition retrieveHintDef(const gchar *tag) { HintDefinition hdef; hdef.tag = tag; hdef.severity = asc_globals_hint_tag_severity(tag); if (hdef.severity == AS_ISSUE_SEVERITY_UNKNOWN) return {}; hdef.explanation = std::string(asc_globals_hint_tag_explanation(tag)); return hdef; } std::string hintToJsonString(const std::string &tag, const std::unordered_map &vars) { // Create YAML document for JSON output fy_document *fyd = fy_document_create(nullptr); if (!fyd) { return "{}"; } fy_node *root = fy_node_create_mapping(fyd); fy_document_set_root(fyd, root); // Add tag field fy_node *tagKey = fy_node_create_scalar(fyd, "tag", FY_NT); fy_node *tagValue = fy_node_create_scalar(fyd, tag.c_str(), FY_NT); fy_node_mapping_append(root, tagKey, tagValue); // Add vars field fy_node *varsKey = fy_node_create_scalar(fyd, "vars", FY_NT); fy_node *varsMapping = fy_node_create_mapping(fyd); fy_node_mapping_append(root, varsKey, varsMapping); for (const auto &[key, value] : vars) { fy_node *varKey = fy_node_create_scalar(fyd, key.c_str(), FY_NT); fy_node *varValue = fy_node_create_scalar(fyd, value.c_str(), FY_NT); fy_node_mapping_append(varsMapping, varKey, varValue); } // Emit as JSON char *json_output = fy_emit_document_to_string(fyd, FYECF_MODE_JSON); std::string result = "{}"; if (json_output) { result = std::string(json_output); free(json_output); } fy_document_destroy(fyd); return result; } } // namespace ASGenerator appstream-generator-0.10.1/src/hintregistry.h000066400000000000000000000042351506754475600212730ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include namespace ASGenerator { /** * Each issue hint type has a severity assigned to it: * * ERROR: A fatal error which resulted in the component being excluded from the final metadata. * WARNING: An issue which did not prevent generating meaningful data, but which is still serious * and should be fixed (warning of this kind usually result in less data). * INFO: Information, no immediate action needed (but will likely be an issue later). * PEDANTIC: Information which may improve the data, but could also be ignored. */ /** * Definition of an issue hint. */ struct HintDefinition { std::string tag; /// Unique issue tag AsIssueSeverity severity; /// Issue severity std::string explanation; /// Explanation template }; /** * Load all issue hints from file and register them globally. */ void loadHintsRegistry(); /** * Save information about all hint templates we know about to a JSON file. */ void saveHintsRegistryToJsonFile(const std::string &fname); /** * Retrieve hint definition for a given tag. */ HintDefinition retrieveHintDef(const char *tag); /** * Convert a hint to JSON format. */ std::string hintToJsonString(const std::string &tag, const std::unordered_map &vars); } // namespace ASGenerator appstream-generator-0.10.1/src/iconhandler.cpp000066400000000000000000001057061506754475600213660ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "iconhandler.h" #include #include #include #include #include #include #include #include #include "config.h" #include "logging.h" #include "result.h" #include "utils.h" namespace fs = std::filesystem; namespace ASGenerator { // all image extensions that we recognize as possible for icons. // the most favorable file extension needs to come first to prefer it inline constexpr std::array PossibleIconExts = {".png", ".svgz", ".svg", ".jxl", ".jpg", ".jpeg", ".gif", ".ico", ".xpm"}; // the image extensions that we will actually allow software to have. inline constexpr std::array AllowedIconExts = {".png", ".jxl", ".svgz", ".svg", ".xpm"}; // Theme implementation Theme::Theme(const std::string &name, const std::vector &indexData) : m_name(name) { g_autoptr(GKeyFile) index = g_key_file_new(); g_autoptr(GError) error = nullptr; std::string indexText(indexData.begin(), indexData.end()); if (!g_key_file_load_from_data(index, indexText.c_str(), indexText.length(), G_KEY_FILE_NONE, &error)) throw std::runtime_error(std::format("Failed to parse theme index for {}: {}", name, error->message)); gsize groupCount = 0; g_auto(GStrv) groups = g_key_file_get_groups(index, &groupCount); for (gsize i = 0; i < groupCount; ++i) { g_autoptr(GError) tmp_error = nullptr; const char *section = groups[i]; // we ignore symbolic icons if (g_str_has_prefix(section, "symbolic/")) continue; int size = g_key_file_get_integer(index, section, "Size", &tmp_error); if (tmp_error) continue; g_autofree gchar *context = g_key_file_get_string(index, section, "Context", &tmp_error); if (tmp_error) continue; int threshold = g_key_file_get_integer(index, section, "Threshold", &tmp_error); if (tmp_error) { threshold = 2; g_clear_error(&tmp_error); } g_autofree gchar *type = g_key_file_get_string(index, section, "Type", &tmp_error); if (tmp_error) { type = g_strdup("Threshold"); g_clear_error(&tmp_error); } int minSize = g_key_file_get_integer(index, section, "MinSize", &tmp_error); if (tmp_error) { minSize = size; g_clear_error(&tmp_error); } int maxSize = g_key_file_get_integer(index, section, "MaxSize", &tmp_error); if (tmp_error) { maxSize = size; g_clear_error(&tmp_error); } int scale = g_key_file_get_integer(index, section, "Scale", &tmp_error); if (tmp_error) { scale = 1; g_clear_error(&tmp_error); } if (size == 0) continue; std::unordered_map> themedir; themedir["path"] = std::string(section); themedir["type"] = std::string(type); themedir["size"] = size; themedir["minsize"] = minSize; themedir["maxsize"] = maxSize; themedir["threshold"] = threshold; themedir["scale"] = scale; m_directories.push_back(std::move(themedir)); } // sort our directory list, so the smallest size is at the top std::ranges::sort(m_directories, [](const auto &a, const auto &b) { return std::get(a.at("size")) < std::get(b.at("size")); }); } Theme::Theme(const std::string &name, std::shared_ptr pkg) { auto indexData = pkg->getFileData(std::format("/usr/share/icons/{}/index.theme", name)); *this = Theme(name, indexData); } const std::string &Theme::name() const { return m_name; } bool Theme::directoryMatchesSize( const std::unordered_map> &themedir, const ImageSize &size, bool assumeThresholdScalable) const { const int scale = std::get(themedir.at("scale")); if (scale != static_cast(size.scale)) return false; const std::string &type = std::get(themedir.at("type")); if (type == "Fixed") return static_cast(size.toInt()) == std::get(themedir.at("size")); if (type == "Scalable") { const int minSize = std::get(themedir.at("minsize")); const int maxSize = std::get(themedir.at("maxsize")); const int sizeInt = static_cast(size.toInt()); if ((minSize <= sizeInt) && (sizeInt <= maxSize)) return true; return false; } if (type == "Threshold") { const int themeSize = std::get(themedir.at("size")); const int th = std::get(themedir.at("threshold")); const int sizeInt = static_cast(size.toInt()); if (assumeThresholdScalable) { // we treat this "Threshold" as if we were allowed to downscale its icons if they // have a higher size. // This can lead to "wrong" scaling, but allows us to retrieve more icons. return themeSize >= sizeInt; } else { // follow the proper algorithm as defined by the XDG spec if (((themeSize - th) <= sizeInt) && (sizeInt <= (themeSize + th))) return true; } return false; } return false; } std::generator Theme::matchingIconFilenames( const std::string &iconName, const ImageSize &size, bool relaxedScalingRules) const { // extensions to check for static const std::array extensions = {"png", "svgz", "svg", "xpm"}; for (const auto &themedir : m_directories) { if (directoryMatchesSize(themedir, size, relaxedScalingRules)) { for (const auto &ext : extensions) { co_yield std::format( "/usr/share/icons/{}/{}/{}.{}", m_name, std::get(themedir.at("path")), iconName, ext); } } } } // IconHandler implementation IconHandler::IconHandler( ContentsStore &ccache, const fs::path &mediaPath, const std::unordered_map> &pkgMap, const std::string &iconTheme) : m_mediaExportPath(mediaPath), m_iconPolicy(nullptr), m_defaultIconSize(64), m_defaultIconState(ASC_ICON_STATE_IGNORED), m_allowIconUpscaling(false), m_allowRemoteIcons(false) { logDebug("Creating new IconHandler"); auto &conf = Config::get(); m_iconFiles.clear(); m_iconPolicy = g_object_ref(conf.iconPolicy()); // sanity checks AscIconPolicyIter policyIter; asc_icon_policy_iter_init(&policyIter, m_iconPolicy); guint iconSize, iconScale; AscIconState iconState; while (asc_icon_policy_iter_next(&policyIter, &iconSize, &iconScale, &iconState)) { if (iconSize == m_defaultIconSize.width && iconScale == m_defaultIconSize.scale) { m_defaultIconState = iconState; break; } } if (m_defaultIconState == ASC_ICON_STATE_IGNORED || m_defaultIconState == ASC_ICON_STATE_REMOTE_ONLY) throw std::runtime_error( "Default icon size '64x64' is set to ignore or remote-only. This is a bug in the generator or " "configuration file."); // cache a list of enabled icon sizes updateEnabledIconSizeList(); m_allowIconUpscaling = conf.feature.allowIconUpscale; // we assume that when screenshot storage is permitted, remote icons are also okay // (only then we can actually use the global media_baseurl, if set) m_allowRemoteIcons = conf.feature.storeScreenshots && !conf.mediaBaseUrl.empty(); // Preseeded theme names. // * prioritize hicolor, because that's where apps often install their upstream icon // * then look at the theme given in the config file // * allow Breeze icon theme, needed to support KDE apps (they have no icon at all, otherwise...) // * in rare events, GNOME needs the same treatment, so special-case Adwaita as well // * We need at least one icon theme to provide the default XDG icon spec stock icons. // A fair take would be to select them between KDE and GNOME at random, but for consistency and // because everyone hates unpredictable behavior, we sort alphabetically and prefer Adwaita over Breeze. m_themeNames = {"hicolor"}; if (!iconTheme.empty()) m_themeNames.push_back(iconTheme); m_themeNames.insert( m_themeNames.end(), { "Adwaita", // GNOME "AdwaitaLegacy", // GNOME "breeze" // KDE }); auto getPackage = [&pkgMap](const std::string &pkid) -> std::shared_ptr { auto it = pkgMap.find(pkid); return (it != pkgMap.end()) ? it->second : nullptr; }; // load data from the contents index. // we don't show mercy to memory here, we just want the icon lookup to be fast, // so we have to cache the data. std::unordered_map> tmpThemes; std::vector pkgKeys; pkgKeys.reserve(pkgMap.size()); for (const auto &[key, _] : pkgMap) pkgKeys.push_back(key); auto filesPkids = ccache.getIconFilesMap(pkgKeys); // Process files in parallel, but synchronize theme and icon file access std::mutex themesMutex, iconFilesMutex; tbb::parallel_for_each(filesPkids.begin(), filesPkids.end(), [&](const auto &info) { const std::string &fname = info.first; const std::string &pkgid = info.second; if (fname.starts_with("/usr/share/pixmaps/")) { auto pkg = getPackage(pkgid); if (pkg) { std::lock_guard lock(iconFilesMutex); m_iconFiles[fname] = std::move(pkg); } return; } // optimization: check if we actually have an interesting path before // entering the slower loop below. if (!fname.starts_with("/usr/share/icons/")) return; auto pkg = getPackage(pkgid); if (!pkg) return; for (const auto &name : m_themeNames) { if (fname == std::format("/usr/share/icons/{}/index.theme", name)) { std::lock_guard lock(themesMutex); tmpThemes[name] = std::make_unique(name, pkg); } else if (fname.starts_with(std::format("/usr/share/icons/{}", name))) { std::lock_guard lock(iconFilesMutex); m_iconFiles[fname] = pkg; } } }); // when running on partial repos (e.g. PPAs) we might not have a package containing the // hicolor theme definition. Since we always need it to be there to properly process icons, // we inject our own copy here. if (tmpThemes.find("hicolor") == tmpThemes.end()) { logInfo("No packaged hicolor icon theme found, using built-in one."); auto hicolorThemeIndex = Utils::getDataPath("hicolor-theme-index.theme"); if (!fs::exists(hicolorThemeIndex)) { logError( "Hicolor icon theme index at '{}' was not found! We will not be able to handle icons in this theme.", hicolorThemeIndex.string()); } else { std::vector indexData; std::ifstream f(hicolorThemeIndex, std::ios::binary); if (f.is_open()) { f.seekg(0, std::ios::end); indexData.resize(f.tellg()); f.seekg(0, std::ios::beg); f.read(reinterpret_cast(indexData.data()), indexData.size()); tmpThemes["hicolor"] = std::make_unique("hicolor", indexData); } } } // this is necessary to keep the ordering (and therefore priority) of themes. // we don't know the order in which we find index.theme files in the code above, // therefore this sorting is necessary. for (const auto &tname : m_themeNames) { auto it = tmpThemes.find(tname); if (it != tmpThemes.end()) m_themes.push_back(std::move(it->second)); } logDebug("Created new IconHandler."); } IconHandler::~IconHandler() { g_object_unref(m_iconPolicy); } void IconHandler::updateEnabledIconSizeList() { m_enabledIconSizes.clear(); AscIconPolicyIter policyIter; asc_icon_policy_iter_init(&policyIter, m_iconPolicy); guint iconSizeInt, iconScale; while (asc_icon_policy_iter_next(&policyIter, &iconSizeInt, &iconScale, nullptr)) m_enabledIconSizes.emplace_back(iconSizeInt, iconSizeInt, iconScale); } std::string IconHandler::getIconNameAndClear(AsComponent *cpt) const { std::string name; // a not-processed icon name is stored as "1x1px" icon, so we can // quickly identify it here. auto icon = Utils::componentGetRawIcon(cpt); if (icon.has_value()) { if (as_icon_get_kind(icon.value()) == AS_ICON_KIND_LOCAL) { const auto filename = as_icon_get_filename(icon.value()); if (filename) name = filename; } else { const auto iconName = as_icon_get_name(icon.value()); if (iconName) name = iconName; } } // clear the list of icons in this component auto iconsArray = as_component_get_icons(cpt); if (iconsArray->len > 0) g_ptr_array_remove_range(iconsArray, 0, iconsArray->len); return name; } bool IconHandler::iconAllowed(const std::string &iconName) { return std::ranges::any_of(AllowedIconExts, [&iconName](const auto &ext) { return iconName.ends_with(ext); }); } std::generator IconHandler::possibleIconFilenames( const std::string &iconName, const ImageSize &size, bool relaxedScalingRules) const { for (const auto &theme : m_themes) { for (const auto &fname : theme->matchingIconFilenames(iconName, size, relaxedScalingRules)) co_yield fname; } if (size.scale == 1 && size.width == 64) { // Check icons root directory for icon files // this is "wrong", but we support it for compatibility reasons. // However, we only ever use it to satisfy the 64x64px requirement for (const auto &extension : PossibleIconExts) co_yield std::format("/usr/share/icons/{}{}", iconName, extension); // check pixmaps directory for icons // we only ever use the pixmap directory contents to satisfy the minimum 64x64px icon // requirement. Otherwise we get weird upscaling to higher sizes or HiDPI sizes happening, // as later code tries to downscale "bigger" sizes. for (const auto &extension : PossibleIconExts) co_yield std::format("/usr/share/pixmaps/{}{}", iconName, extension); } } std::unordered_map IconHandler::findIcons( const std::string &iconName, const std::vector &sizes, std::shared_ptr pkg) const { std::unordered_map sizeMap; for (const auto &size : sizes) { // search for possible icon filenames, using relaxed scaling rules by default for (const auto &fname : possibleIconFilenames(iconName, size, true)) { if (pkg) { // we are supposed to search in one particular package const auto &contents = pkg->contents(); if (std::ranges::find(contents, fname) != contents.end()) { sizeMap[size] = IconFindResult(pkg, fname); break; } } else { // global search in all packages auto it = m_iconFiles.find(fname); if (it == m_iconFiles.end()) continue; sizeMap[size] = IconFindResult(it->second, fname); break; } } } return sizeMap; } std::string IconHandler::stripIconExt(const std::string &iconName) { if (iconName.ends_with(".png")) return iconName.substr(0, iconName.length() - 4); if (iconName.ends_with(".svg")) return iconName.substr(0, iconName.length() - 4); if (iconName.ends_with(".xpm")) return iconName.substr(0, iconName.length() - 4); if (iconName.ends_with(".svgz")) return iconName.substr(0, iconName.length() - 5); return iconName; } bool IconHandler::storeIcon( AsComponent *cpt, GeneratorResult &gres, const fs::path &cptExportPath, std::shared_ptr sourcePkg, const std::string &iconPath, const ImageSize &size, AscIconState targetState) const { auto iformat = asc_image_format_from_filename(iconPath.c_str()); if (iformat == ASC_IMAGE_FORMAT_UNKNOWN) { gres.addHint( as_component_get_id(cpt), "icon-format-unsupported", { {"icon_fname", fs::path(iconPath).filename()} }); return false; } auto path = cptExportPath / "icons" / size.toString(); auto iconName = (gres.getPackage()->kind() == PackageKind::Fake) ? fs::path(iconPath).filename().string() : std::format("{}_{}", gres.getPackage()->name(), fs::path(iconPath).filename().string()); if (iconName.ends_with(".svgz")) iconName = iconName.substr(0, iconName.length() - 5) + ".png"; else if (iconName.ends_with(".svg")) iconName = iconName.substr(0, iconName.length() - 4) + ".png"; else if (iconName.ends_with(".xpm")) iconName = iconName.substr(0, iconName.length() - 4) + ".png"; auto iconStoreLocation = path / iconName; if (fs::exists(iconStoreLocation)) { // we already extracted that icon, skip the extraction step // and just add the new icon. if (targetState != ASC_ICON_STATE_REMOTE_ONLY) { g_autoptr(AsIcon) icon = as_icon_new(); as_icon_set_kind(icon, AS_ICON_KIND_CACHED); as_icon_set_width(icon, size.width); as_icon_set_height(icon, size.height); as_icon_set_scale(icon, size.scale); as_icon_set_name(icon, iconName.c_str()); as_component_add_icon(cpt, icon); } if (targetState != ASC_ICON_STATE_CACHED_ONLY && m_allowRemoteIcons) { auto gcid = gres.gcidForComponent(cpt); if (gcid.empty()) { gres.addHint( cpt, "internal-error", "No global ID could be found for the component, could not add remote icon."); return true; } auto remoteIconUrl = fs::path(gcid) / "icons" / size.toString() / iconName; g_autoptr(AsIcon) icon = as_icon_new(); as_icon_set_kind(icon, AS_ICON_KIND_REMOTE); as_icon_set_width(icon, size.width); as_icon_set_height(icon, size.height); as_icon_set_scale(icon, size.scale); as_icon_set_url(icon, remoteIconUrl.string().c_str()); as_component_add_icon(cpt, icon); } return true; } // filepath is checked because icon can reside in another binary // eg amarok's icon is in amarok-data std::vector iconData; try { iconData = sourcePkg->getFileData(iconPath); } catch (const std::exception &e) { gres.addHint( as_component_get_id(cpt), "pkg-extract-error", { {"fname", fs::path(iconPath).filename() }, {"pkg_fname", fs::path(sourcePkg->getFilename()).filename()}, {"error", e.what() } }); return false; } if (iconData.empty()) { gres.addHint( as_component_get_id(cpt), "pkg-empty-file", { {"fname", fs::path(iconPath).filename() }, {"pkg_fname", fs::path(sourcePkg->getFilename()).filename()} }); return false; } auto scaled_width = (int)size.width * (int)size.scale; auto scaled_height = (int)size.height * (int)size.scale; if ((iformat == ASC_IMAGE_FORMAT_SVG) || (iformat == ASC_IMAGE_FORMAT_SVGZ)) { // create target directory fs::create_directories(path); g_autoptr(GError) error = nullptr; g_autoptr(GInputStream) stream = g_memory_input_stream_new_from_data(iconData.data(), iconData.size(), nullptr); gboolean ret = asc_render_svg_to_file( G_INPUT_STREAM(stream), (gint)scaled_width, (gint)scaled_height, ASC_IMAGE_FORMAT_PNG, iconStoreLocation.c_str(), &error); if (!ret) { gres.addHint( as_component_get_id(cpt), "image-write-error", { {"fname", fs::path(iconPath).filename() }, {"pkg_fname", fs::path(sourcePkg->getFilename()).filename()}, {"error", error->message } }); return false; } } else { g_autoptr(GError) error = nullptr; g_autoptr(AscImage) img = asc_image_new_from_data( iconData.data(), iconData.size(), -1, -1, ASC_IMAGE_LOAD_FLAG_NONE, iconPath.ends_with(".svgz") ? ASC_IMAGE_FORMAT_SVGZ : ASC_IMAGE_FORMAT_UNKNOWN, &error); if (!img) { gres.addHint( as_component_get_id(cpt), "image-write-error", { {"fname", fs::path(iconPath).filename() }, {"pkg_fname", fs::path(sourcePkg->getFilename()).filename()}, {"error", error->message } }); return false; } if (iformat == ASC_IMAGE_FORMAT_XPM) { // we use XPM images only if they are large enough if (m_allowIconUpscaling) { // we only try upscaling for the default 64x64px size and only if // the icon is not too small if (size != ImageSize(64)) return false; if ((asc_image_get_width(img) < 48) || (asc_image_get_height(img) < 48)) return false; } else { if ((asc_image_get_width(img) < scaled_width) || (asc_image_get_height(img) < scaled_height)) return false; } } // ensure that we don't try to make an application visible that has a really tiny icon // by upscaling it to a blurry mess if (size.scale == 1 && size.width == 64) { if ((asc_image_get_width(img) < 48) || (asc_image_get_height(img) < 48)) { gres.addHint( cpt, "icon-too-small", { {"icon_name", iconName}, {"icon_size", std::format("{}x{}", asc_image_get_width(img), asc_image_get_height(img))} }); return false; } } // warn about icon upscaling, it looks ugly if (scaled_width > asc_image_get_width(img)) { gres.addHint( cpt, "icon-scaled-up", { {"icon_name", iconName}, {"icon_size", std::format("{}x{}", asc_image_get_width(img), asc_image_get_height(img))}, {"scale_size", size.toString()} }); } // create target directory fs::create_directories(path); asc_image_scale(img, scaled_width, scaled_height); asc_image_save_filename(img, iconStoreLocation.c_str(), 0, 0, ASC_IMAGE_SAVE_FLAG_OPTIMIZE, &error); if (error) { gres.addHint( cpt, "image-write-error", { {"fname", fs::path(iconPath).filename() }, {"pkg_fname", fs::path(sourcePkg->getFilename()).filename()}, {"error", error->message } }); return false; } } if (targetState != ASC_ICON_STATE_REMOTE_ONLY) { g_autoptr(AsIcon) icon = as_icon_new(); as_icon_set_kind(icon, AS_ICON_KIND_CACHED); as_icon_set_width(icon, size.width); as_icon_set_height(icon, size.height); as_icon_set_scale(icon, size.scale); as_icon_set_name(icon, iconName.c_str()); as_component_add_icon(cpt, icon); } if (targetState != ASC_ICON_STATE_CACHED_ONLY && m_allowRemoteIcons) { auto gcid = gres.gcidForComponent(cpt); if (gcid.empty()) { gres.addHint( cpt, "internal-error", "No global ID could be found for the component, could not add remote icon."); return true; } auto remoteIconUrl = fs::path(gcid) / "icons" / size.toString() / iconName; g_autoptr(AsIcon) icon = as_icon_new(); as_icon_set_kind(icon, AS_ICON_KIND_REMOTE); as_icon_set_width(icon, size.width); as_icon_set_height(icon, size.height); as_icon_set_scale(icon, size.scale); as_icon_set_url(icon, remoteIconUrl.string().c_str()); as_component_add_icon(cpt, icon); } return true; } IconHandler::IconFindResult IconHandler::findIconScalableToSize( const std::unordered_map &possibleIcons, const ImageSize &size) const { IconFindResult info; // on principle, never attempt to up- or downscale an icon to something below // AppStream's default icon size. // The clients can do that just as well, without us wasting disk space // and network bandwidth. if (size.scale == 1 && size.width < 64) return info; // the size we want wasn't found, can we downscale a larger one? for (const auto &[asize, data] : possibleIcons) { if (asize.scale != size.scale) continue; if (asize < size) continue; info = data; break; } if (!info.pkg && m_allowIconUpscaling && (size == m_defaultIconSize)) { // no icon was found to downscale, but we allow upscaling, so try one last time // to find a suitable icon for at least the default AppStream icon size. for (const auto &[asize, data] : possibleIcons) { // we never allow icons smaller than 48x48px if (asize.width < 48) continue; if (asize.scale != size.scale) continue; info = data; break; } } return info; } bool IconHandler::process(GeneratorResult &gres, AsComponent *cpt) { std::lock_guard lock(m_mutex); // we don't touch fonts unless those didn't have their icon // rendered from the font itself already if (as_component_get_kind(cpt) == AS_COMPONENT_KIND_FONT) { auto iconsArr = as_component_get_icons(cpt); for (guint i = 0; i < iconsArr->len; i++) { auto icon = AS_ICON(g_ptr_array_index(iconsArr, i)); // nothing to do for us if cached and remote icons are already there if (as_icon_get_kind(icon) == AS_ICON_KIND_CACHED || as_icon_get_kind(icon) == AS_ICON_KIND_REMOTE) return true; } } auto iconName = getIconNameAndClear(cpt); // nothing to do if there is no icon if (iconName.empty()) return true; auto gcid = gres.gcidForComponent(cpt); if (gcid.empty()) { const char *cid = as_component_get_id(cpt); if (!cid) cid = "general"; gres.addHint(cid, "internal-error", "No global ID could be found for the component."); return false; } auto cptMediaPath = m_mediaExportPath / gcid; if (iconName.starts_with("/")) { logDebug("Looking for icon '{}' for '{}::{}' (path)", iconName, gres.pkid(), as_component_get_id(cpt)); const auto &contents = gres.getPackage()->contents(); if (std::ranges::find(contents, iconName) != contents.end()) { return storeIcon( cpt, gres, cptMediaPath, gres.getPackage(), iconName, m_defaultIconSize, m_defaultIconState); } // we couldn't find the absolute icon path gres.addHint( as_component_get_id(cpt), "icon-not-found", { {"icon_fname", iconName} }); return false; } else { logDebug("Looking for icon '{}' for '{}::{}' (XDG)", iconName, gres.pkid(), as_component_get_id(cpt)); iconName = fs::path(iconName).filename(); // Small hack: Strip .png and other extensions from icon files to make the XDG and Pixmap finder // work properly, which add their own icon extensions and find the most suitable icon. // The icon name should not have an extension anyway, but some apps ignore the desktop-entry spec... iconName = stripIconExt(iconName); std::string lastIconName; /// Search for an icon in XDG icon directories. /// Returns true on success and sets lastIconName to the /// last icon name that has been handled. auto findAndStoreXdgIcon = [&](std::shared_ptr epkg = nullptr) -> bool { auto iconRes = findIcons(iconName, m_enabledIconSizes, std::move(epkg)); if (iconRes.empty()) return false; std::unordered_map iconsStored; AscIconPolicyIter policyIter; asc_icon_policy_iter_init(&policyIter, m_iconPolicy); guint iconSizeInt, iconScale; AscIconState iconState; while (asc_icon_policy_iter_next(&policyIter, &iconSizeInt, &iconScale, &iconState)) { const ImageSize size(iconSizeInt, iconSizeInt, iconScale); auto infoIt = iconRes.find(size); IconFindResult info; if (infoIt == iconRes.end()) info.pkg = nullptr; else info = infoIt->second; // check if we can scale another size to the desired one if (!info.pkg) info = findIconScalableToSize(iconRes, size); // give up if we still haven't found an icon (in which case `info.pkg` would be set) if (!info.pkg) continue; lastIconName = info.fname; if (iconAllowed(lastIconName)) { if (storeIcon(cpt, gres, cptMediaPath, info.pkg, lastIconName, size, iconState)) iconsStored[size] = std::move(info); } else { // the found icon is not suitable, but maybe we can scale a differently sized icon to the right one? info = findIconScalableToSize(iconRes, size); if (!info.pkg) continue; if (iconAllowed(info.fname)) { if (storeIcon(cpt, gres, cptMediaPath, info.pkg, lastIconName, size, iconState)) iconsStored[size] = info; lastIconName = info.fname; } } if (gres.isIgnored(cpt)) { // running storeIcon() in this loop may lead to rejection // of this component, in case icons can't be saved. // we just give up in that case. return false; } } // ensure we have stored a 64x64px icon, since this is mandated // by the AppStream spec by downscaling a larger icon that we // might have found. if (iconsStored.find(ImageSize(64)) != iconsStored.end()) { logDebug("Found icon {} - {} in XDG directories, 64x64px size is present", gres.pkid(), iconName); return true; } else { for (const auto &size : m_enabledIconSizes) { auto it = iconsStored.find(size); if (it == iconsStored.end()) continue; if (size < ImageSize(64)) continue; logInfo( "Downscaling icon {} - {} from {} to {}", gres.pkid(), iconName, size.toString(), m_defaultIconSize.toString()); const auto &info = it->second; lastIconName = info.fname; if (storeIcon( cpt, gres, cptMediaPath, info.pkg, lastIconName, m_defaultIconSize, m_defaultIconState)) { return true; } } } // if we are here, we either didn't find an icon, or no icon is present // in the default 64x64px size return false; }; // search for the right icon inside the current package auto success = findAndStoreXdgIcon(gres.getPackage()); if (!success && !gres.isIgnored(cpt)) { // search in all packages success = findAndStoreXdgIcon(); } if (success) { logDebug("Icon {} - {} found in XDG dirs", gres.pkid(), iconName); // we found a valid stock icon, so set that additionally to the cached one g_autoptr(AsIcon) icon = as_icon_new(); as_icon_set_kind(icon, AS_ICON_KIND_STOCK); as_icon_set_name(icon, iconName.c_str()); as_component_add_icon(cpt, icon); return true; } else { logDebug("Icon {} - {} not found in required size(s) in XDG dirs", gres.pkid(), iconName); if (!lastIconName.empty() && !iconAllowed(lastIconName)) { gres.addHint( as_component_get_id(cpt), "icon-format-unsupported", { {"icon_fname", fs::path(lastIconName).filename()} }); return false; } gres.addHint( as_component_get_id(cpt), "icon-not-found", { {"icon_fname", iconName} }); return false; } } } } // namespace ASGenerator appstream-generator-0.10.1/src/iconhandler.h000066400000000000000000000132001506754475600210160ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include #include #include #include #include "utils.h" #include "backends/interfaces.h" #include "contentsstore.h" namespace ASGenerator { class Config; class GeneratorResult; /** * Describes an icon theme as specified in the XDG theme spec. */ class Theme { public: explicit Theme(const std::string &name, const std::vector &indexData); explicit Theme(const std::string &name, std::shared_ptr pkg); const std::string &name() const; const auto &directories() const { return m_directories; } /** * Check if a directory is suitable for the selected size. * If @assumeThresholdScalable is set to true, we will allow * downscaling of any higher-than-requested icon size, even if the * section is of "Threshold" type and would usually prohibit the scaling. */ bool directoryMatchesSize( const std::unordered_map> &themedir, const ImageSize &size, bool assumeThresholdScalable = false) const; /** * Returns a generator of possible icon filenames that match @iconName and @size. * If @relaxedScalingRules is set to true, we scale down any bigger icon size, even * if the theme definition would usually prohibit that. */ std::generator matchingIconFilenames( const std::string &iconName, const ImageSize &size, bool relaxedScalingRules = false) const; private: std::string m_name; std::vector>> m_directories; }; /** * Finds icons in a software archive and stores them in the * correct sizes for a given AppStream component. */ class IconHandler { public: IconHandler( ContentsStore &ccache, const fs::path &mediaPath, const std::unordered_map> &pkgMap, const std::string &iconTheme = ""); ~IconHandler(); /** * Try to find & store icons for a selected component. */ bool process(GeneratorResult &gres, AsComponent *cpt); static bool iconAllowed(const std::string &iconName); // Delete copy constructor and assignment operator IconHandler(const IconHandler &) = delete; IconHandler &operator=(const IconHandler &) = delete; private: fs::path m_mediaExportPath; std::vector> m_themes; std::unordered_map> m_iconFiles; std::vector m_themeNames; AscIconPolicy *m_iconPolicy; ImageSize m_defaultIconSize; AscIconState m_defaultIconState; std::vector m_enabledIconSizes; bool m_allowIconUpscaling; bool m_allowRemoteIcons; mutable std::mutex m_mutex; void updateEnabledIconSizeList(); std::string getIconNameAndClear(AsComponent *cpt) const; /** * Generates potential filenames of the icon that is searched for in the * given size. */ std::generator possibleIconFilenames( const std::string &iconName, const ImageSize &size, bool relaxedScalingRules = false) const; /** * Helper structure for the findIcons method. */ struct IconFindResult { std::shared_ptr pkg; std::string fname; IconFindResult() = default; IconFindResult(std::shared_ptr p, std::string f) : pkg(std::move(p)), fname(std::move(f)) { } }; /** * Looks up 'icon' with 'size' in popular icon themes according to the XDG * icon theme spec. */ std::unordered_map findIcons( const std::string &iconName, const std::vector &sizes, std::shared_ptr pkg = nullptr) const; /** * Strip file extension from icon. */ static std::string stripIconExt(const std::string &iconName); /** * Extracts the icon from the package and stores it in the cache. * Ensures the stored icon always has the size given in "size", and renders * scalable vectorgraphics if necessary. */ bool storeIcon( AsComponent *cpt, GeneratorResult &gres, const fs::path &cptExportPath, std::shared_ptr sourcePkg, const std::string &iconPath, const ImageSize &size, AscIconState targetState) const; /** * Helper function to try to find an icon that we can up- or downscale to the desired size. */ IconFindResult findIconScalableToSize( const std::unordered_map &possibleIcons, const ImageSize &size) const; }; } // namespace ASGenerator appstream-generator-0.10.1/src/logging.cpp000066400000000000000000000066401506754475600205230ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "logging.h" #include #include #include #include #include #include #include #include namespace ASGenerator { // helper to avoid the "static initialization order fiasco" static std::atomic_bool &verboseFlag() noexcept { static std::atomic_bool flag{false}; return flag; } void setVerbose(bool verbose) noexcept { verboseFlag().store(verbose, std::memory_order_relaxed); } bool isVerbose() noexcept { return verboseFlag().load(std::memory_order_relaxed); } constexpr std::string_view logSeverityToString(LogSeverity severity) noexcept { switch (severity) { case LogSeverity::DEBUG: return "DEBUG"; case LogSeverity::INFO: return "INFO"; case LogSeverity::WARNING: return "WARNING"; case LogSeverity::ERROR: return "ERROR"; default: return "UNKNOWN"; } } void logMessageImpl(LogSeverity severity, const std::string &message) { // Use synchronized output stream for thread safety std::osyncstream sync_out{std::cout}; auto now = std::chrono::system_clock::now(); auto time_t = std::chrono::system_clock::to_time_t(now); auto tm = *std::localtime(&time_t); std::ostringstream time_stream; time_stream << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); sync_out << time_stream.str() << " - " << logSeverityToString(severity) << ": " << message << '\n'; } static void printTextbox( std::string_view title, std::string_view tl, std::string_view hline, std::string_view tr, std::string_view vline, std::string_view bl, std::string_view br) { const auto tlen = title.length(); const auto hline_count = 10 + tlen; std::string output; output.reserve(128 + tlen + hline_count * hline.length() * 2); // Rough estimate // Top line output += '\n'; output += tl; for (size_t i = 0; i < hline_count; ++i) output += hline; output += tr; output += '\n'; // Middle line with title output += vline; output += " "; output += title; output += std::string(8, ' '); output += vline; output += '\n'; // Bottom line output += bl; for (size_t i = 0; i < hline_count; ++i) output += hline; output += br; output += '\n'; std::cout.write(output.data(), output.size()); std::cout.flush(); } void printHeaderBox(std::string_view title) { printTextbox(title, "╔", "═", "╗", "║", "╚", "╝"); } void printSectionBox(std::string_view title) { printTextbox(title, "┌", "─", "┐", "│", "└", "┘"); } } // namespace ASGenerator appstream-generator-0.10.1/src/logging.h000066400000000000000000000060461506754475600201700ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include namespace ASGenerator { enum class LogSeverity { DEBUG, INFO, WARNING, ERROR }; void setVerbose(bool verbose) noexcept; bool isVerbose() noexcept; constexpr std::string_view logSeverityToString(LogSeverity severity) noexcept; // Base logging function that handles the actual output void logMessageImpl(LogSeverity severity, const std::string &message); template void logMessage(LogSeverity severity, std::string_view fmt, Args &&...args) { std::string formatted_msg; if constexpr (sizeof...(Args) > 0) formatted_msg = std::vformat(fmt, std::make_format_args(args...)); else formatted_msg = std::string{fmt}; logMessageImpl(severity, formatted_msg); } template inline void logDebug(std::string_view fmt, Args &&...args) { if (isVerbose()) logMessage(LogSeverity::DEBUG, fmt, std::forward(args)...); } template inline void logInfo(std::string_view fmt, Args &&...args) { logMessage(LogSeverity::INFO, fmt, std::forward(args)...); } template inline void logWarning(std::string_view fmt, Args &&...args) { logMessage(LogSeverity::WARNING, fmt, std::forward(args)...); } template inline void logError(std::string_view fmt, Args &&...args) { logMessage(LogSeverity::ERROR, fmt, std::forward(args)...); } // Convenience overloads for simple string messages (no template arguments) inline void logDebug(std::string_view msg) { if (isVerbose()) logMessageImpl(LogSeverity::DEBUG, std::string{msg}); } inline void logInfo(std::string_view msg) { logMessageImpl(LogSeverity::INFO, std::string{msg}); } inline void logWarning(std::string_view msg) { logMessageImpl(LogSeverity::WARNING, std::string{msg}); } inline void logError(std::string_view msg) { logMessageImpl(LogSeverity::ERROR, std::string{msg}); } /** * Print a header box with the given title to stdout. * @param title Title text */ void printHeaderBox(std::string_view title); /** * Print a section box with the given title to stdout. * @param title Title text */ void printSectionBox(std::string_view title); } // namespace ASGenerator appstream-generator-0.10.1/src/main.cpp000066400000000000000000000245241506754475600200220ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "defines.h" #include #include #include #include #include #include #include #include #include #ifdef HAVE_BACKWARD #define BACKWARD_HAS_UNWIND 1 #include #endif #include "logging.h" #include "config.h" #include "engine.h" #include "utils.h" using namespace ASGenerator; /** * Create XDG runtime directory if it doesn't exist. * Ubuntu's Snappy package manager doesn't create the runtime * data dir, and some of the libraries we depend on expect it * to be available. Try to create the directory. */ static void createXdgRuntimeDir() { const char *xdgRuntimeDir = g_getenv("XDG_RUNTIME_DIR"); if (!xdgRuntimeDir || xdgRuntimeDir[0] != '/') return; // nothing to do here if (fs::exists(xdgRuntimeDir)) return; // directory already exists try { fs::create_directories(xdgRuntimeDir); // Set permissions to 700 (owner read/write/execute only) if (chmod(xdgRuntimeDir, S_IRWXU) == -1) logDebug("Failed to set permissions on XDG runtime dir: {}", std::strerror(errno)); } catch (const std::filesystem::filesystem_error &e) { logWarning("Unable to create XDG runtime dir: {}", e.what()); return; } logDebug("Created missing XDG runtime dir: {}", xdgRuntimeDir); } /** * Print version information to stdout. */ static void printVersion() { std::cout << "Generator version: " << ASGEN_VERSION << std::endl; } /** * Ensure that suite and/or section parameters are set correctly. */ static void ensureSuiteAndOrSectionParameterSet(const std::vector &args) { if (args.size() < 3) { std::cerr << "Invalid number of parameters: You need to specify at least a suite name." << std::endl; std::exit(1); } if (args.size() > 4) { std::cerr << "Invalid number of parameters: You need to specify a suite name and (optionally) a section name." << std::endl; std::exit(1); } } /** * Execute the specified command with the given arguments. */ static int executeCommand(const std::string &command, const std::vector &args, bool forceAction) { auto engine = std::make_unique(); engine->setForced(forceAction); if (command == "run" || command == "process") { if (args.size() == 2) { // process all suites engine->run(); } else { ensureSuiteAndOrSectionParameterSet(args); if (args.size() == 3) engine->run(args[2]); else engine->run(args[2], args[3]); } } else if (command == "process-file") { if (args.size() < 5) { std::cerr << "Invalid number of parameters: You need to specify a suite name, a section name and at least " "one file to process." << std::endl; return 1; } std::vector files(args.begin() + 4, args.end()); engine->processFile(args[2], args[3], files); } else if (command == "publish") { ensureSuiteAndOrSectionParameterSet(args); if (args.size() == 3) engine->publish(args[2]); else engine->publish(args[2], args[3]); } else if (command == "cleanup") { engine->runCleanup(); } else if (command == "remove-found") { if (args.size() != 3) { std::cerr << "Invalid number of parameters: You need to specify a suite name." << std::endl; return 1; } engine->removeHintsComponents(args[2]); } else if (command == "forget") { if (args.size() != 3) { std::cerr << "Invalid number of parameters: You need to specify a package-id (partial IDs are allowed)." << std::endl; return 1; } engine->forgetPackage(args[2]); } else if (command == "info") { if (args.size() != 3) { std::cerr << "Invalid number of parameters: You need to specify a package-id." << std::endl; return 1; } engine->printPackageInfo(args[2]); } else { std::cerr << std::format("The command '{}' is unknown.", command) << std::endl; return 1; } return 0; } /** * Main function */ int main(int argc, char **argv) { gboolean verbose = FALSE; gboolean showHelp = FALSE; gboolean showVersion = FALSE; gboolean forceAction = FALSE; g_autofree gchar *wdir = nullptr; g_autofree gchar *exportDir = nullptr; g_autofree gchar *configFname = nullptr; // Initialize locale for proper UTF-8 handling if (!setlocale(LC_ALL, "")) { // If system locale fails, try to set a UTF-8 locale explicitly logInfo("No locale set, falling back to C.UTF-8."); if (!setlocale(LC_ALL, "C.UTF-8") && !setlocale(LC_ALL, "en_US.UTF-8")) logWarning("Warning: Could not set UTF-8 locale. UTF-8 text may be corrupted."); } // Make sure nothing localizes numbers by accident std::setlocale(LC_NUMERIC, "C"); try { std::locale loc(""); std::locale::global(loc); std::cout.imbue(loc); std::cerr.imbue(loc); } catch (...) { // Non-fatal; iostreams will just use the classic locale } #ifdef HAVE_BACKWARD backward::SignalHandling sh; if (sh.loaded()) logDebug("Backward registered for stack-trace printing."); #endif GOptionEntry entries[] = { {"help", 'h', 0, G_OPTION_ARG_NONE, &showHelp, "Show help options", nullptr}, {"verbose", 0, 0, G_OPTION_ARG_NONE, &verbose, "Show extra debugging information", nullptr}, {"version", 0, 0, G_OPTION_ARG_NONE, &showVersion, "Show the program version", nullptr}, {"force", 0, 0, G_OPTION_ARG_NONE, &forceAction, "Force action", nullptr}, {"workspace", 'w', 0, G_OPTION_ARG_STRING, &wdir, "Define the workspace location", "DIR"}, {"config", 'c', 0, G_OPTION_ARG_STRING, &configFname, "Use the given configuration file", "FILE"}, {"export-dir", 0, 0, G_OPTION_ARG_STRING, &exportDir, "Override the workspace root export directory", "DIR"}, {nullptr} }; g_autoptr(GError) error = nullptr; g_autoptr(GOptionContext) context = g_option_context_new(" - AppStream Generator"); // Set program description g_option_context_set_description( context, "Subcommands:\n" " run [SUITE] [SECTION] - Process new metadata for the given distribution suite and publish it.\n" " process-file SUITE SECTION FILE1 [FILE2 ...]\n" " - Process new metadata for the given package file.\n" " cleanup - Cleanup old metadata and media files.\n" " publish SUITE [SECTION] - Export all metadata and publish reports in the export directories.\n" " remove-found SUITE - Drop all valid processed metadata and hints.\n" " forget PKID - Drop all information we have about this (partial) package-id.\n" " info PKID - Show information associated with this (full) package-id.\n"); g_option_context_set_summary(context, "AppStream Metadata Generator"); g_option_context_add_main_entries(context, entries, nullptr); g_option_context_set_help_enabled(context, TRUE); if (!g_option_context_parse(context, &argc, &argv, &error)) { std::cerr << "Unable to parse parameters: " << error->message << std::endl; return 1; } if (showHelp) { g_autofree gchar *helpText = g_option_context_get_help(context, TRUE, nullptr); std::cout << helpText << std::endl; return 0; } if (showVersion) { printVersion(); return 0; } if (argc < 2) { std::cerr << "No subcommand specified!" << std::endl; g_autofree gchar *helpText = g_option_context_get_help(context, TRUE, nullptr); std::cerr << helpText << std::endl; return 1; } // Convert remaining arguments to vector of strings std::vector args; args.reserve(argc); for (int i = 0; i < argc; ++i) args.emplace_back(argv[i]); // globally enable verbose mode, if requested if (verbose) setVerbose(true); auto &conf = Config::get(); std::string configFilename; if (configFname) { configFilename = configFname; } else { // if we don't have an explicit config file set, and also no // workspace, take the current directory std::string workspaceDir; if (wdir) { workspaceDir = wdir; } else { workspaceDir = fs::current_path().string(); } configFilename = fs::path(workspaceDir) / "asgen-config.json"; } std::string workspaceDirStr = wdir ? wdir : ""; std::string exportDirStr = exportDir ? exportDir : ""; try { conf.loadFromFile(configFilename, workspaceDirStr, exportDirStr); } catch (const std::exception &e) { std::cerr << std::format("Unable to load configuration: {}", e.what()) << std::endl; return 4; } // ensure runtime dir exists, in case we are installed with Snappy createXdgRuntimeDir(); int result = 0; if (isVerbose()) { result = executeCommand(args[1], args, forceAction); } else { try { result = executeCommand(args[1], args, forceAction); } catch (const std::exception &e) { std::cerr << std::format("Error executing command: {}", e.what()) << std::endl; result = 1; } } return result; } appstream-generator-0.10.1/src/meson.build000066400000000000000000000033501506754475600205260ustar00rootroot00000000000000 subdir('backends') asgencpp_src = files( 'config.cpp', 'contentsstore.cpp', 'cptmodifiers.cpp', 'datainjectpkg.cpp', 'datastore.cpp', 'dataunits.cpp', 'downloader.cpp', 'engine.cpp', 'extractor.cpp', 'hintregistry.cpp', 'iconhandler.cpp', 'logging.cpp', 'reportgenerator.cpp', 'result.cpp', 'utils.cpp', 'yaml-utils.cpp', 'zarchive.cpp', 'backends/interfaces.cpp', ) asgencpp_hdr = files( 'config.h', 'contentsstore.h', 'cptmodifiers.h', 'datainjectpkg.h', 'datastore.h', 'dataunits.h', 'downloader.h', 'engine.h', 'extractor.h', 'hintregistry.h', 'iconhandler.h', 'logging.h', 'reportgenerator.h', 'result.h', 'utils.h', 'yaml-utils.h', 'zarchive.h', 'backends/interfaces.h', ) conf_data = configuration_data() conf_data.set_quoted('DATADIR', join_paths(get_option('prefix'), get_option('datadir'), 'appstream')) conf_data.set_quoted('ASGEN_VERSION', asgen_version) conf_data.set('HAVE_BACKWARD', backward_dep.found()) defines_h = configure_file( output: 'defines.h', configuration: conf_data ) asgen_deps = [ glib_dep, appstream_dep, ascompose_dep, fyaml_dep, lmdb_dep, archive_dep, curl_dep, tbb_dep, icu_dep, inja_dep, libxml2_dep, ] asgen_args = [ '-DI_KNOW_THE_APPSTREAM_COMPOSE_API_IS_SUBJECT_TO_CHANGE' ] asgen_lib = static_library( 'asgenlib', [asgencpp_src, asgencpp_hdr, defines_h, backends_src], dependencies: asgen_deps, cpp_args: asgen_args, ) asgen_lib_dep = declare_dependency( link_with: asgen_lib, include_directories: [src_dir], dependencies: asgen_deps, compile_args: asgen_args, ) asgen_exe = executable('appstream-generator', ['main.cpp'], dependencies: [ asgen_lib_dep, backward_deps, ], install: true ) appstream-generator-0.10.1/src/reportgenerator.cpp000066400000000000000000000722041506754475600223160ustar00rootroot00000000000000/* * Copyright (C) 2016-2022 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "reportgenerator.h" #include #include #include #include #include #include #include #include #include #include #include #include "defines.h" #include "logging.h" #include "utils.h" #include "hintregistry.h" namespace ASGenerator { ReportGenerator::ReportGenerator(DataStore *db) : m_dstore(db), m_conf(&Config::get()), m_templateDir(m_conf->templateDir()), m_injaEnv( m_conf->templateDir().empty() ? inja::Environment() : inja::Environment(m_conf->templateDir().string() + "/")) { // Enable searching for included templates in files if we have a template directory m_injaEnv.set_search_included_templates_in_files(!m_conf->templateDir().empty()); m_htmlExportDir = m_conf->htmlExportDir; m_mediaPoolDir = m_dstore->mediaExportPoolDir(); m_mediaPoolUrl = std::format("{}/pool", m_conf->mediaBaseUrl); m_defaultTemplateDir = Utils::getDataPath("templates/default"); m_versionInfo = std::format("{}, AS: {}", ASGEN_VERSION, as_version_string()); } void ReportGenerator::setupInjaContext(inja::json &context) { auto now = std::chrono::system_clock::now(); auto time_t = std::chrono::system_clock::to_time_t(now); auto *tm = std::localtime(&time_t); auto timeStr = std::format( "{:04d}-{:02d}-{:02d} {:02d}:{:02d} [{}]", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_zone ? tm->tm_zone : "UTC"); context["time"] = timeStr; context["generator_version"] = m_versionInfo; context["project_name"] = m_conf->projectName; context["root_url"] = m_conf->htmlBaseUrl; } void ReportGenerator::renderPage(const std::string &pageID, const std::string &exportName, const inja::json &context) { inja::json fullContext = context; setupInjaContext(fullContext); auto fname = m_htmlExportDir / (exportName + ".html"); fs::create_directories(fname.parent_path()); auto templatePath = m_templateDir / (pageID + ".html"); auto defaultTemplatePath = m_defaultTemplateDir / (pageID + ".html"); inja::Environment *activeEnv = &m_injaEnv; std::unique_ptr defaultEnv; if (!fs::exists(templatePath) && fs::exists(defaultTemplatePath)) { defaultEnv = std::make_unique(m_defaultTemplateDir.string() + "/"); // Configure the default environment to search for included templates in files defaultEnv->set_search_included_templates_in_files(true); activeEnv = defaultEnv.get(); } logDebug("Rendering HTML page: {}", exportName); try { auto data = activeEnv->render_file(pageID + ".html", fullContext); std::ofstream f(fname); f << data; f.close(); } catch (const std::exception &e) { logError("Failed to render template {}: {}", pageID, e.what()); } } void ReportGenerator::renderPagesFor(const std::string &suiteName, const std::string §ion, const DataSummary &dsum) { if (m_templateDir.empty()) { logError("Can not render HTML: No page templates found."); return; } logInfo("Rendering HTML for {}/{}", suiteName, section); std::regex maintRE(R"([àáèéëêòöøîìùñ~/\\(\\" '])"); // write issue hint pages for (const auto &[pkgname, pkgHEntries] : dsum.hintEntries) { auto exportName = std::format("{}/{}/issues/{}", suiteName, section, pkgname); inja::json context; context["suite"] = suiteName; context["package_name"] = pkgname; context["section"] = section; inja::json entries = inja::json::array(); for (const auto &[cid, hentry] : pkgHEntries) { inja::json entry; entry["component_id"] = cid; inja::json architectures = inja::json::array(); for (const auto &arch : hentry.archs) { architectures.push_back(inja::json{ {"arch", arch} }); } entry["architectures"] = architectures; entry["has_errors"] = false; if (!hentry.errors.empty()) { entry["has_errors"] = true; inja::json errors = inja::json::array(); for (const auto &error : hentry.errors) { errors.push_back(inja::json{ {"error_tag", error.tag }, {"error_description", error.message} }); } entry["errors"] = errors; } entry["has_warnings"] = false; if (!hentry.warnings.empty()) { entry["has_warnings"] = true; inja::json warnings = inja::json::array(); for (const auto &warning : hentry.warnings) { warnings.push_back(inja::json{ {"warning_tag", warning.tag }, {"warning_description", warning.message} }); } entry["warnings"] = warnings; } entry["has_infos"] = false; if (!hentry.infos.empty()) { entry["has_infos"] = true; inja::json infos = inja::json::array(); for (const auto &info : hentry.infos) { infos.push_back(inja::json{ {"info_tag", info.tag }, {"info_description", info.message} }); } entry["infos"] = infos; } entries.push_back(entry); } context["entries"] = entries; renderPage("issues_page", exportName, context); } // write metadata info pages for (const auto &[pkgname, pkgMVerEntries] : dsum.mdataEntries) { auto exportName = std::format("{}/{}/metainfo/{}", suiteName, section, pkgname); inja::json context; context["suite"] = suiteName; context["package_name"] = pkgname; context["section"] = section; inja::json cpts = inja::json::array(); for (const auto &[ver, mEntries] : pkgMVerEntries) { for (const auto &[gcid, mentry] : mEntries) { inja::json cpt; cpt["component_id"] = std::format("{} - {}", mentry.identifier, ver); inja::json architectures = inja::json::array(); for (const auto &arch : mentry.archs) { architectures.push_back(inja::json{ {"arch", arch} }); } cpt["architectures"] = architectures; cpt["metadata"] = Utils::escapeXml( mentry.data); // FIXME: Set html-autoescape in Inja once we can depend on a newer version, and don't // use explicit escapeXml() here auto cptMediaPath = m_mediaPoolDir / gcid; auto cptMediaUrl = std::format("{}/{}", m_mediaPoolUrl, gcid); std::string iconUrl; switch (mentry.kind) { case AS_COMPONENT_KIND_UNKNOWN: iconUrl = std::format("{}/{}/{}/{}", m_conf->htmlBaseUrl, "static", "img", "no-image.png"); break; case AS_COMPONENT_KIND_DESKTOP_APP: case AS_COMPONENT_KIND_WEB_APP: case AS_COMPONENT_KIND_FONT: case AS_COMPONENT_KIND_OPERATING_SYSTEM: { auto iconPath = cptMediaPath / "icons" / "64x64" / mentry.iconName; if (fs::exists(iconPath)) { iconUrl = std::format("{}/{}/{}/{}", cptMediaUrl, "icons", "64x64", mentry.iconName); } else { iconUrl = std::format("{}/{}/{}/{}", m_conf->htmlBaseUrl, "static", "img", "no-image.png"); } break; } default: iconUrl = std::format("{}/{}/{}/{}", m_conf->htmlBaseUrl, "static", "img", "cpt-nogui.png"); break; } cpt["icon_url"] = iconUrl; cpts.push_back(cpt); } } context["cpts"] = cpts; renderPage("metainfo_page", exportName, context); } // write hint overview page auto hindexExportName = std::format("{}/{}/issues/index", suiteName, section); inja::json hsummaryCtx; hsummaryCtx["suite"] = suiteName; hsummaryCtx["section"] = section; inja::json summaries = inja::json::array(); for (const auto &[maintainer, pkgSummariesMap] : dsum.pkgSummaries) { inja::json summary; summary["maintainer"] = maintainer; summary["maintainer_anchor"] = std::regex_replace(maintainer, maintRE, "_"); bool interesting = false; inja::json packages = inja::json::array(); for (const auto &[pkgname, pkgSummary] : pkgSummariesMap) { if ((pkgSummary.infoCount == 0) && (pkgSummary.warningCount == 0) && (pkgSummary.errorCount == 0)) continue; interesting = true; inja::json pkg; pkg["pkgname"] = pkgSummary.pkgname; // use conditionals for count display if (pkgSummary.infoCount > 0) pkg["has_info_count"] = true; if (pkgSummary.warningCount > 0) pkg["has_warning_count"] = true; if (pkgSummary.errorCount > 0) pkg["has_error_count"] = true; pkg["info_count"] = pkgSummary.infoCount; pkg["warning_count"] = pkgSummary.warningCount; pkg["error_count"] = pkgSummary.errorCount; packages.push_back(pkg); } if (interesting) { summary["packages"] = packages; summaries.push_back(summary); } } hsummaryCtx["summaries"] = summaries; renderPage("issues_index", hindexExportName, hsummaryCtx); // write metainfo overview page auto mindexExportName = std::format("{}/{}/metainfo/index", suiteName, section); inja::json msummaryCtx; msummaryCtx["suite"] = suiteName; msummaryCtx["section"] = section; inja::json metaSummaries = inja::json::array(); for (const auto &[maintainer, pkgSummariesMap] : dsum.pkgSummaries) { inja::json metaSummary; metaSummary["maintainer"] = maintainer; metaSummary["maintainer_anchor"] = std::regex_replace(maintainer, maintRE, "_"); inja::json packages = inja::json::array(); for (const auto &[pkgname, pkgSummary] : pkgSummariesMap) { if (pkgSummary.cpts.empty()) continue; inja::json pkg; pkg["pkgname"] = pkgSummary.pkgname; inja::json components = inja::json::array(); for (const auto &cid : pkgSummary.cpts) { components.push_back(inja::json{ {"cid", cid} }); } pkg["components"] = components; packages.push_back(pkg); } metaSummary["packages"] = packages; metaSummaries.push_back(metaSummary); } msummaryCtx["summaries"] = metaSummaries; renderPage("metainfo_index", mindexExportName, msummaryCtx); // render section index page auto secIndexExportName = std::format("{}/{}/index", suiteName, section); inja::json secIndexCtx; secIndexCtx["suite"] = suiteName; secIndexCtx["section"] = section; float percOne = 100.0f / static_cast(dsum.totalMetadata + dsum.totalInfos + dsum.totalWarnings + dsum.totalErrors); secIndexCtx["valid_percentage"] = dsum.totalMetadata * percOne; secIndexCtx["info_percentage"] = dsum.totalInfos * percOne; secIndexCtx["warning_percentage"] = dsum.totalWarnings * percOne; secIndexCtx["error_percentage"] = dsum.totalErrors * percOne; secIndexCtx["metainfo_count"] = dsum.totalMetadata; secIndexCtx["error_count"] = dsum.totalErrors; secIndexCtx["warning_count"] = dsum.totalWarnings; secIndexCtx["info_count"] = dsum.totalInfos; renderPage("section_page", secIndexExportName, secIndexCtx); } ReportGenerator::DataSummary ReportGenerator::preprocessInformation( const std::string &suiteName, const std::string §ion, const std::vector> &pkgs) { DataSummary dsum; logInfo("Collecting data about hints and available metainfo for {}/{}", suiteName, section); auto dtype = m_conf->metadataType; g_autoptr(AsMetadata) mdata = as_metadata_new(); as_metadata_set_format_style(mdata, AS_FORMAT_STYLE_CATALOG); as_metadata_set_format_version(mdata, m_conf->formatVersion); for (const auto &pkg : pkgs) { const auto &pkid = pkg->id(); auto gcids = m_dstore->getGCIDsForPackage(pkid); auto hintsData = m_dstore->getHints(pkid); if (gcids.empty() && hintsData.empty()) continue; PkgSummary pkgsummary; bool newInfo = false; pkgsummary.pkgname = pkg->name(); auto maintainerIt = dsum.pkgSummaries.find(pkg->maintainer()); if (maintainerIt != dsum.pkgSummaries.end()) { auto pkgIt = maintainerIt->second.find(pkg->name()); if (pkgIt != maintainerIt->second.end()) { pkgsummary = pkgIt->second; } else { newInfo = true; } } // process component metadata for this package if there are any if (!gcids.empty()) { for (const auto &gcid : gcids) { auto cidOpt = Utils::getCidFromGlobalID(gcid); if (!cidOpt.has_value()) continue; auto cid = cidOpt.value(); // don't add the same entry multiple times for multiple versions auto pkgIt = dsum.mdataEntries.find(pkg->name()); if (pkgIt != dsum.mdataEntries.end()) { auto verIt = pkgIt->second.find(pkg->ver()); if (verIt != pkgIt->second.end()) { auto meIt = verIt->second.find(gcid); if (meIt == verIt->second.end()) { // this component is new dsum.totalMetadata += 1; newInfo = true; } else { // we already have a component with this gcid auto &archs = meIt->second.archs; if (std::find(archs.begin(), archs.end(), pkg->arch()) == archs.end()) { archs.push_back(pkg->arch()); } continue; } } } else { // we will add a new component dsum.totalMetadata += 1; } MetadataEntry me; me.identifier = cid; me.data = m_dstore->getMetadata(dtype, gcid); as_metadata_clear_components(mdata); g_autoptr(GError) error = nullptr; if (dtype == DataType::YAML) as_metadata_parse_data(mdata, me.data.c_str(), -1, AS_FORMAT_KIND_YAML, &error); else as_metadata_parse_data(mdata, me.data.c_str(), -1, AS_FORMAT_KIND_XML, &error); if (error != nullptr) { logWarning("Failed to parse metadata for {}: {}", gcid, error->message); continue; } auto cpt = as_metadata_get_component(mdata); if (cpt != nullptr) { const auto iconsArr = as_component_get_icons(cpt); assert(iconsArr != nullptr); for (guint i = 0; i < iconsArr->len; i++) { AsIcon *icon = AS_ICON(g_ptr_array_index(iconsArr, i)); if (as_icon_get_kind(icon) == AS_ICON_KIND_CACHED) { me.iconName = as_icon_get_name(icon); break; } } me.kind = as_component_get_kind(cpt); } else { me.kind = AS_COMPONENT_KIND_UNKNOWN; } me.archs.push_back(pkg->arch()); dsum.mdataEntries[pkg->name()][pkg->ver()][gcid] = std::move(me); pkgsummary.cpts.emplace_back(std::format("{} - {}", cid, pkg->ver())); } } // process hints for this package, if there are any if (!hintsData.empty()) { try { auto hintsJson = inja::json::parse(hintsData); if (!hintsJson.contains("hints") || !hintsJson["hints"].is_object()) continue; auto hintsNode = hintsJson["hints"]; // Iterate through component IDs in hints for (const auto &[cid, jhintsNode] : hintsNode.items()) { HintEntry he; // don't add the same hints multiple times for multiple versions and architectures auto pkgIt = dsum.hintEntries.find(pkg->name()); if (pkgIt != dsum.hintEntries.end()) { auto heIt = pkgIt->second.find(cid); if (heIt != pkgIt->second.end()) { he = heIt->second; // we already have hints for this component ID he.archs.push_back(pkg->arch()); // TODO: check if we have the same hints - if not, create a new entry. continue; } newInfo = true; } else { newInfo = true; } he.identifier = cid; if (jhintsNode.is_array()) { // Iterate through hints array for (const auto &jhintNode : jhintsNode) { if (!jhintNode.is_object()) continue; // Get tag if (!jhintNode.contains("tag") || !jhintNode["tag"].is_string()) continue; std::string tag = jhintNode["tag"]; g_autoptr(AscHint) hint = nullptr; g_autoptr(GError) error = nullptr; hint = asc_hint_new_for_tag(tag.c_str(), &error); if (hint == nullptr) { logError( "Encountered invalid tag '{}' in component '{}' of package '{}': {}", tag, cid, pkid, error ? error->message : "Unknown error"); // emit an internal error, invalid tags shouldn't happen tag = "internal-unknown-tag"; hint = asc_hint_new_for_tag(tag.c_str(), nullptr); } // render the full message using the static template and data from the hint if (jhintNode.contains("vars") && jhintNode["vars"].is_object()) { for (const auto &[varKey, varValue] : jhintNode["vars"].items()) { if (varValue.is_string()) { std::string varValueStr = varValue; asc_hint_add_explanation_var(hint, varKey.c_str(), varValueStr.c_str()); } } } g_autofree gchar *msg = asc_hint_format_explanation(hint); const auto severity = asc_hint_get_severity(hint); // add the new hint to the right category if (severity == AS_ISSUE_SEVERITY_INFO) { he.infos.emplace_back(tag, msg); pkgsummary.infoCount++; } else if (severity == AS_ISSUE_SEVERITY_WARNING) { he.warnings.emplace_back(tag, msg); pkgsummary.warningCount++; } else if (severity == AS_ISSUE_SEVERITY_PEDANTIC) { // We ignore pedantic issues completely for now } else { he.errors.emplace_back(tag, msg); pkgsummary.errorCount++; } } } if (newInfo) he.archs.push_back(pkg->arch()); dsum.hintEntries[pkg->name()][he.identifier] = he; } } catch (const std::exception &e) { logError("Failed to parse hints JSON for package {}: {}", pkid, e.what()); } } dsum.pkgSummaries[pkg->maintainer()][pkg->name()] = pkgsummary; if (newInfo) { dsum.totalInfos += pkgsummary.infoCount; dsum.totalWarnings += pkgsummary.warningCount; dsum.totalErrors += pkgsummary.errorCount; } } return dsum; } void ReportGenerator::saveStatistics(const std::string &suiteName, const std::string §ion, const DataSummary &dsum) { std::unordered_map> statsData = { {"suite", suiteName }, {"section", section }, {"totalInfos", dsum.totalInfos }, {"totalWarnings", dsum.totalWarnings}, {"totalErrors", dsum.totalErrors }, {"totalMetadata", dsum.totalMetadata} }; m_dstore->addStatistics(statsData); } void ReportGenerator::exportStatistics() { logInfo("Exporting statistical data."); // return all statistics we have from the database auto statsCollection = m_dstore->getStatistics(); // Sort statsCollection by timestamp in ascending order std::sort(statsCollection.begin(), statsCollection.end(), [](const auto &a, const auto &b) -> bool { return a.time < b.time; }); std::unordered_map>>> suiteData; // Group data by suite and section for (const auto &entry : statsCollection) { const auto &js = entry.data; const auto timestamp = static_cast(entry.time); // Extract suite and section from the data std::string suite, section; int64_t totalErrors = 0, totalWarnings = 0, totalInfos = 0, totalMetadata = 0; auto suiteIt = js.find("suite"); if (suiteIt != js.end() && std::holds_alternative(suiteIt->second)) suite = std::get(suiteIt->second); auto sectionIt = js.find("section"); if (sectionIt != js.end() && std::holds_alternative(sectionIt->second)) section = std::get(sectionIt->second); auto errorsIt = js.find("totalErrors"); if (errorsIt != js.end() && std::holds_alternative(errorsIt->second)) totalErrors = std::get(errorsIt->second); auto warningsIt = js.find("totalWarnings"); if (warningsIt != js.end() && std::holds_alternative(warningsIt->second)) totalWarnings = std::get(warningsIt->second); auto infosIt = js.find("totalInfos"); if (infosIt != js.end() && std::holds_alternative(infosIt->second)) totalInfos = std::get(infosIt->second); auto metadataIt = js.find("totalMetadata"); if (metadataIt != js.end() && std::holds_alternative(metadataIt->second)) totalMetadata = std::get(metadataIt->second); if (suite.empty() || section.empty()) continue; // Store data points suiteData[suite][section + "_errors"].push_back({timestamp, totalErrors}); suiteData[suite][section + "_warnings"].push_back({timestamp, totalWarnings}); suiteData[suite][section + "_infos"].push_back({timestamp, totalInfos}); suiteData[suite][section + "_metadata"].push_back({timestamp, totalMetadata}); } inja::json jsonOutput = inja::json::object(); for (const auto &[suiteName, sections] : suiteData) { // Group by section std::unordered_map>>> sectionGroups; for (const auto &[key, data] : sections) { auto underscorePos = key.rfind('_'); if (underscorePos != std::string::npos) { std::string sectionName = key.substr(0, underscorePos); std::string dataType = key.substr(underscorePos + 1); sectionGroups[sectionName][dataType] = data; } } inja::json suiteJson = inja::json::object(); for (const auto &[sectionName, dataTypes] : sectionGroups) { inja::json sectionJson = inja::json::object(); for (const auto &[dataType, dataPoints] : dataTypes) { inja::json dataArray = inja::json::array(); for (const auto &point : dataPoints) { dataArray.push_back(inja::json::array({point[0], point[1]})); } sectionJson[dataType] = dataArray; } suiteJson[sectionName] = sectionJson; } jsonOutput[suiteName] = suiteJson; } auto fname = fs::path(m_htmlExportDir) / "statistics.json"; fs::create_directories(fname.parent_path()); std::ofstream sf(fname); // Use dump() without indentation for compact output sf << jsonOutput.dump(); sf.flush(); sf.close(); } void ReportGenerator::processFor( const std::string &suiteName, const std::string §ion, const std::vector> &pkgs) { // collect all needed information and save statistics auto dsum = preprocessInformation(suiteName, section, pkgs); saveStatistics(suiteName, section, dsum); // drop old pages auto suitSecPagesDest = fs::path(m_htmlExportDir) / suiteName / section; if (fs::exists(suitSecPagesDest)) fs::remove_all(suitSecPagesDest); // render fresh info pages renderPagesFor(suiteName, section, dsum); } void ReportGenerator::updateIndexPages() { logInfo("Updating HTML index pages and static data."); // render main overview inja::json context; // Get sorted suites auto suites = m_conf->suites; std::sort(suites.begin(), suites.end(), [](const Suite &a, const Suite &b) { return a.name > b.name; }); inja::json suitesArray = inja::json::array(); for (const auto &suite : suites) { suitesArray.push_back(inja::json{ {"suite", suite.name} }); inja::json secCtx; secCtx["suite"] = suite.name; inja::json sectionsArray = inja::json::array(); for (const auto §ion : suite.sections) { sectionsArray.push_back(inja::json{ {"section", section} }); } secCtx["sections"] = sectionsArray; renderPage("sections_index", std::format("{}/index", suite.name), secCtx); } context["suites"] = suitesArray; // Get sorted old suites auto oldsuites = m_conf->oldsuites; std::sort(oldsuites.begin(), oldsuites.end()); inja::json oldsuitesArray = inja::json::array(); for (const auto &suite : oldsuites) { inja::json oldsuite; oldsuite["suite"] = suite; oldsuitesArray.push_back(oldsuite); } context["oldsuites"] = oldsuitesArray; renderPage("main", "index", context); // copy static data, if present auto staticSrcDir = fs::path(m_templateDir) / "static"; if (fs::exists(staticSrcDir)) { auto staticDestDir = fs::path(m_htmlExportDir) / "static"; if (fs::exists(staticDestDir)) fs::remove_all(staticDestDir); Utils::copyDir(staticSrcDir, staticDestDir); } } } // namespace ASGenerator appstream-generator-0.10.1/src/reportgenerator.h000066400000000000000000000074041506754475600217630ustar00rootroot00000000000000/* * Copyright (C) 2016-2022 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include #include "config.h" #include "datastore.h" #include "backends/interfaces.h" namespace ASGenerator { class ReportGenerator { public: explicit ReportGenerator(DataStore *db); ~ReportGenerator() = default; void processFor( const std::string &suiteName, const std::string §ion, const std::vector> &pkgs); void updateIndexPages(); void exportStatistics(); // Delete copy constructor and assignment operator ReportGenerator(const ReportGenerator &) = delete; ReportGenerator &operator=(const ReportGenerator &) = delete; // Public structs for testing access struct HintTag { std::string tag; std::string message; }; struct HintEntry { std::string identifier; std::vector archs; std::vector errors; std::vector warnings; std::vector infos; }; struct MetadataEntry { AsComponentKind kind; std::string identifier; std::vector archs; std::string data; std::string iconName; }; struct PkgSummary { std::string pkgname; std::vector cpts; int infoCount = 0; int warningCount = 0; int errorCount = 0; }; struct DataSummary { // maintainer -> package -> summary std::unordered_map> pkgSummaries; // package -> component_id -> hint_entry std::unordered_map> hintEntries; // package -> version -> gcid -> entry std::unordered_map>> mdataEntries; int64_t totalMetadata = 0; int64_t totalInfos = 0; int64_t totalWarnings = 0; int64_t totalErrors = 0; }; // Public methods for testing access void setupInjaContext(inja::json &context); void renderPage(const std::string &pageID, const std::string &exportName, const inja::json &context); void renderPagesFor(const std::string &suiteName, const std::string §ion, const DataSummary &dsum); DataSummary preprocessInformation( const std::string &suiteName, const std::string §ion, const std::vector> &pkgs); void saveStatistics(const std::string &suiteName, const std::string §ion, const DataSummary &dsum); private: DataStore *m_dstore; Config *m_conf; fs::path m_htmlExportDir; fs::path m_templateDir; fs::path m_defaultTemplateDir; fs::path m_mediaPoolDir; std::string m_mediaPoolUrl; std::string m_versionInfo; inja::Environment m_injaEnv; }; } // namespace ASGenerator appstream-generator-0.10.1/src/result.cpp000066400000000000000000000224201506754475600204050ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "result.h" #include #include #include #include #include "hintregistry.h" #include "logging.h" #include "yaml-utils.h" namespace ASGenerator { GeneratorResult::GeneratorResult(std::shared_ptr pkg) : m_pkg(std::move(pkg)), m_res(asc_result_new()) { asc_result_set_bundle_kind(m_res, AS_BUNDLE_KIND_PACKAGE); asc_result_set_bundle_id(m_res, m_pkg->name().c_str()); } GeneratorResult::GeneratorResult(AscResult *result, std::shared_ptr pkg) : m_pkg(std::move(pkg)) { m_res = g_object_ref(result); asc_result_set_bundle_kind(m_res, AS_BUNDLE_KIND_PACKAGE); asc_result_set_bundle_id(m_res, m_pkg->name().c_str()); } GeneratorResult::~GeneratorResult() { if (m_res) g_object_unref(m_res); } GeneratorResult::GeneratorResult(GeneratorResult &&other) noexcept : m_pkg(std::move(other.m_pkg)), m_res(other.m_res) { other.m_res = nullptr; } GeneratorResult &GeneratorResult::operator=(GeneratorResult &&other) noexcept { if (this != &other) { if (m_res) g_object_unref(m_res); m_pkg = std::move(other.m_pkg); m_res = other.m_res; other.m_res = nullptr; } return *this; } std::string GeneratorResult::pkid() const { return m_pkg->id(); } bool GeneratorResult::addHint( const std::string &id, const std::string &tag, const std::unordered_map &vars) { std::string cid = id.empty() ? "general" : id; if (vars.empty()) return asc_result_add_hint_by_cid(m_res, cid.c_str(), tag.c_str(), nullptr, nullptr) != 0; // create null-terminated argument list for variadic function std::vector args; for (const auto &[key, value] : vars) { args.push_back(const_cast(key.c_str())); args.push_back(const_cast(value.c_str())); } args.push_back(nullptr); // null terminator return asc_result_add_hint_by_cid_v(m_res, cid.c_str(), tag.c_str(), args.data()) != 0; } bool GeneratorResult::addHint( AsComponent *cpt, const std::string &tag, const std::unordered_map &vars) { std::string cid = cpt ? as_component_get_id(cpt) : "general"; return addHint(cid, tag, vars); } bool GeneratorResult::addHint(const std::string &id, const std::string &tag, const std::string &msg) { std::unordered_map vars; if (!msg.empty()) { vars["msg"] = msg; } return addHint(id, tag, vars); } bool GeneratorResult::addHint(AsComponent *cpt, const std::string &tag, const std::string &msg) { std::string cid = cpt ? as_component_get_id(cpt) : "general"; return addHint(cid, tag, msg); } void GeneratorResult::addComponentWithString(AsComponent *cpt, const std::string &data) { g_autoptr(GError) error = nullptr; if (!asc_result_add_component_with_string(m_res, cpt, data.c_str(), &error)) throw std::runtime_error(error->message); } std::string GeneratorResult::hintsToJson() const { if (hintsCount() == 0) { return ""; } // Create the root document auto doc = Yaml::createDocument(); if (!doc) { logError("Failed to create YAML document for hints"); return ""; } // Create root mapping fy_node *root = fy_node_create_mapping(doc.get()); fy_document_set_root(doc.get(), root); // Add package field fy_node *pkgKey = fy_node_create_scalar(doc.get(), "package", FY_NT); fy_node *pkgValue = fy_node_create_scalar_copy(doc.get(), pkid().c_str(), FY_NT); fy_node_mapping_append(root, pkgKey, pkgValue); // Create hints mapping fy_node *hintsKey = fy_node_create_scalar(doc.get(), "hints", FY_NT); fy_node *hintsMapping = fy_node_create_mapping(doc.get()); fy_node_mapping_append(root, hintsKey, hintsMapping); // Get component IDs with hints auto componentIds = getComponentIdsWithHints(); for (const auto &cid : componentIds) { // Get hints for this component GPtrArray *cptHints = asc_result_get_hints(m_res, cid.c_str()); if (!cptHints || cptHints->len == 0) continue; // Create sequence for this component's hints fy_node *cidKey = fy_node_create_scalar(doc.get(), cid.c_str(), FY_NT); fy_node *hintSequence = fy_node_create_sequence(doc.get()); fy_node_mapping_append(hintsMapping, cidKey, hintSequence); for (guint i = 0; i < cptHints->len; i++) { auto hint = static_cast(g_ptr_array_index(cptHints, i)); // Create mapping for this hint fy_node *hintMapping = fy_node_create_mapping(doc.get()); fy_node_sequence_append(hintSequence, hintMapping); // Add tag const char *tag = asc_hint_get_tag(hint); fy_node *tagKey = fy_node_create_scalar(doc.get(), "tag", FY_NT); fy_node *tagValue = fy_node_create_scalar(doc.get(), tag, FY_NT); fy_node_mapping_append(hintMapping, tagKey, tagValue); // Add vars GPtrArray *varsList = asc_hint_get_explanation_vars_list(hint); if (varsList && varsList->len > 0) { fy_node *varsKey = fy_node_create_scalar(doc.get(), "vars", FY_NT); fy_node *varsMapping = fy_node_create_mapping(doc.get()); fy_node_mapping_append(hintMapping, varsKey, varsMapping); for (guint j = 0; j < varsList->len; j += 2) { if (j + 1 < varsList->len) { const char *key = static_cast(g_ptr_array_index(varsList, j)); const char *value = static_cast(g_ptr_array_index(varsList, j + 1)); fy_node *varKey = fy_node_create_scalar(doc.get(), key, FY_NT); fy_node *varValue = fy_node_create_scalar(doc.get(), value, FY_NT); fy_node_mapping_append(varsMapping, varKey, varValue); } } } } } // Emit as JSON g_autofree gchar *json_output = fy_emit_document_to_string(doc.get(), FYECF_MODE_JSON); std::string result; if (json_output) result = std::string(json_output); return result; } std::uint32_t GeneratorResult::hintsCount() const { return asc_result_hints_count(m_res); } std::uint32_t GeneratorResult::componentsCount() const { return asc_result_components_count(m_res); } std::vector GeneratorResult::getComponentIdsWithHints() const { g_autofree const gchar **cids = asc_result_get_component_ids_with_hints(m_res); std::vector result; if (cids) { for (int i = 0; cids[i] != nullptr; ++i) result.emplace_back(cids[i]); } return result; } bool GeneratorResult::hasHint(const std::string &componentId, const std::string &tag) const { // Find the component by ID first GPtrArray *hints = asc_result_get_hints(m_res, componentId.c_str()); if (!hints) return false; for (guint i = 0; i < hints->len; i++) { AscHint *hint = ASC_HINT(g_ptr_array_index(hints, i)); if (asc_hint_get_tag(hint) == tag) return true; } return false; } bool GeneratorResult::hasHint(AsComponent *cpt, const std::string &tag) const { if (!cpt) return hasHint("general", tag); return asc_result_has_hint(m_res, cpt, tag.c_str()) != 0; } void GeneratorResult::addComponent(AsComponent *cpt) const { asc_result_add_component(m_res, cpt, nullptr, nullptr); } void GeneratorResult::removeComponent(AsComponent *cpt) const { asc_result_remove_component(m_res, cpt); } bool GeneratorResult::isIgnored(AsComponent *cpt) const { return asc_result_is_ignored(m_res, cpt) != 0; } bool GeneratorResult::isUnitIgnored() const { return asc_result_unit_ignored(m_res); } std::string GeneratorResult::gcidForComponent(AsComponent *cpt) const { const char *gcid = asc_result_gcid_for_component(m_res, cpt); std::string result; if (gcid) { result = gcid; } return result; } std::vector GeneratorResult::getComponentGcids() const { g_autofree const char **gcids = asc_result_get_component_gcids(m_res); std::vector result; if (gcids) { for (int i = 0; gcids[i] != nullptr; ++i) result.emplace_back(gcids[i]); } return result; } GPtrArray *GeneratorResult::fetchComponents() const { return asc_result_fetch_components(m_res); } } // namespace ASGenerator appstream-generator-0.10.1/src/result.h000066400000000000000000000115601506754475600200550ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include "backends/interfaces.h" typedef struct _AscResult AscResult; namespace ASGenerator { /** * Represents the result of processing a package for AppStream metadata generation. * This class ties together a package instance and compose result. */ class GeneratorResult { public: /** * Constructor with package. */ explicit GeneratorResult(std::shared_ptr pkg); /** * Constructor with result and package. */ GeneratorResult(AscResult *result, std::shared_ptr pkg); /** * Destructor. */ ~GeneratorResult(); // Delete copy constructor and assignment operator GeneratorResult(const GeneratorResult &) = delete; GeneratorResult &operator=(const GeneratorResult &) = delete; // Move constructor and assignment operator GeneratorResult(GeneratorResult &&other) noexcept; GeneratorResult &operator=(GeneratorResult &&other) noexcept; /** * Get the package ID. */ std::string pkid() const; /** * Get the package instance. */ std::shared_ptr getPackage() const { return m_pkg; } /** * Get the AscResult instance. */ AscResult *getResult() const { return m_res; } /** * Add an issue hint to this result. * @param id The component-id or component itself this tag is assigned to. * @param tag The hint tag. * @param vars Dictionary of parameters to insert into the issue report. * @return True if the hint did not cause the removal of the component, False otherwise. */ bool addHint( const std::string &id, const std::string &tag, const std::unordered_map &vars = {}); /** * Add an issue hint to this result. * @param cpt The component this tag is assigned to. * @param tag The hint tag. * @param vars Dictionary of parameters to insert into the issue report. * @return True if the hint did not cause the removal of the component, False otherwise. */ bool addHint( AsComponent *cpt, const std::string &tag, const std::unordered_map &vars = {}); /** * Add an issue hint to this result with a simple message. * @param id The component-id or component itself this tag is assigned to. * @param tag The hint tag. * @param msg An error message to add to the report. * @return True if the hint did not cause the removal of the component, False otherwise. */ bool addHint(const std::string &id, const std::string &tag, const std::string &msg); /** * Add an issue hint to this result with a simple message. * @param cpt The component this tag is assigned to. * @param tag The hint tag. * @param msg An error message to add to the report. * @return True if the hint did not cause the removal of the component, False otherwise. */ bool addHint(AsComponent *cpt, const std::string &tag, const std::string &msg); /** * Create JSON metadata for the hints found for the package * associated with this GeneratorResult. */ std::string hintsToJson() const; // Delegate methods to AscResult std::uint32_t hintsCount() const; std::uint32_t componentsCount() const; std::vector getComponentIdsWithHints() const; bool hasHint(const std::string &componentId, const std::string &tag) const; bool hasHint(AsComponent *cpt, const std::string &tag) const; void addComponent(AsComponent *cpt) const; void addComponentWithString(AsComponent *cpt, const std::string &data); void removeComponent(AsComponent *cpt) const; bool isIgnored(AsComponent *cpt) const; bool isUnitIgnored() const; std::string gcidForComponent(AsComponent *cpt) const; std::vector getComponentGcids() const; GPtrArray *fetchComponents() const; private: std::shared_ptr m_pkg; AscResult *m_res; }; } // namespace ASGenerator appstream-generator-0.10.1/src/utils.cpp000066400000000000000000000342471506754475600202410ustar00rootroot00000000000000/* * Copyright (C) 2016-2022 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "defines.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logging.h" #include "downloader.h" namespace ASGenerator { inline constexpr const char *DESKTOP_GROUP = "Desktop Entry"; ImageSize::ImageSize(const std::string &str) : width(0), height(0), scale(0) { auto sep = str.find('x'); if (sep == std::string::npos || sep == 0) return; auto scaleSep = str.find('@'); width = std::stoul(str.substr(0, sep)); if (scaleSep == std::string::npos) { scale = 1; height = std::stoul(str.substr(sep + 1)); } else { if (scaleSep == str.length() - 1) throw std::runtime_error("Image size string must not end with '@'."); height = std::stoul(str.substr(sep + 1, scaleSep - sep - 1)); scale = std::stoul(str.substr(scaleSep + 1)); } } std::string ImageSize::toString() const { if (scale == 1) return std::format("{}x{}", width, height); else return std::format("{}x{}@{}", width, height, scale); } std::uint32_t ImageSize::toInt() const { if (width > height) return width * scale; return height * scale; } namespace Utils { std::string randomString(std::uint32_t len) { if (len == 0) len = 1; const std::string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, chars.size() - 1); std::string result; result.reserve(len); for (std::uint32_t i = 0; i < len; ++i) { result += chars[dis(gen)]; } return result; } bool localeValid(const std::string &locale) { return locale != "x-test" && locale != "xx"; } bool isTopLevelDomain(const std::string &value) { if (value.empty()) return false; return as_utils_is_tld(value.c_str()); } std::optional getCidFromGlobalID(const std::string &gcid) { std::vector parts; std::stringstream ss(gcid); std::string item; while (std::getline(ss, item, '/')) parts.push_back(item); if (parts.size() != 4) return std::nullopt; if (isTopLevelDomain(parts[0])) { return std::format("{}.{}.{}", parts[0], parts[1], parts[2]); } return parts[2]; } void hardlink(const fs::path &srcPath, const fs::path &destPath) { if (::link(srcPath.c_str(), destPath.c_str()) != 0) throw std::runtime_error(std::format("Unable to create link: {}", std::strerror(errno))); } void copyFile(const fs::path &srcFile, const fs::path &destFile, bool useHardlinks, bool followSymlinks) { std::error_code ec; fs::path dest_dir = destFile.parent_path(); if (!fs::exists(dest_dir)) { fs::create_directories(dest_dir, ec); if (ec) throw std::runtime_error(std::format("Failed to create directory {}: {}", dest_dir.string(), ec.message())); } fs::copy_options copy_opts = fs::copy_options::overwrite_existing; if (!followSymlinks) copy_opts |= fs::copy_options::copy_symlinks; if (useHardlinks) { hardlink(srcFile, destFile); return; } fs::copy_file(srcFile, destFile, copy_opts, ec); if (ec) { throw std::runtime_error( std::format("Failed to copy {} to {}: {}", srcFile.string(), destFile.string(), ec.message())); } } void copyDir(const std::string &srcDir, const std::string &destDir, bool useHardlinks, bool followSymlinks) { const fs::path srcPath(srcDir); const fs::path destPath(destDir); if (!fs::exists(srcPath)) throw std::runtime_error(std::format("Source path {} does not exist.", srcPath.string())); // Handle single file case first if (!fs::is_directory(srcPath)) { copyFile(srcPath, destPath, useHardlinks, followSymlinks); return; } std::error_code ec; if (!fs::exists(destPath)) { fs::create_directories(destPath, ec); if (ec) throw std::runtime_error( std::format("Error creating destination directory {}: {}", destPath.string(), ec.message())); } fs::copy_options copy_opts = fs::copy_options::recursive; if (!followSymlinks) copy_opts |= fs::copy_options::copy_symlinks; if (useHardlinks) copy_opts |= fs::copy_options::create_hard_links; fs::copy(srcPath, destPath, copy_opts, ec); if (ec) { throw std::runtime_error( std::format("Failed to copy directory {} to {}: {}", srcPath.string(), destPath.string(), ec.message())); } } fs::path getExecutableDir() { char result[PATH_MAX]; ssize_t count = readlink("/proc/self/exe", result, PATH_MAX); if (count == -1) throw std::runtime_error("Failed to get executable path"); std::string exePath(result, count); return fs::path(exePath).parent_path(); } fs::path getDataPath(const std::string &fname) { static const auto exeDirName = getExecutableDir(); // useful for testing if (!exeDirName.string().starts_with("/usr")) { auto resPath = exeDirName / ".." / ".." / "data" / fname; if (fs::exists(resPath)) return fs::canonical(resPath); resPath = exeDirName / ".." / ".." / ".." / "data" / fname; if (fs::exists(resPath)) return fs::canonical(resPath); resPath = exeDirName / ".." / ".." / ".." / ".." / "data" / fname; if (fs::exists(resPath)) return fs::canonical(resPath); } auto resPath = fs::path(DATADIR) / fname; if (fs::exists(resPath)) return resPath; resPath = exeDirName / ".." / "data" / fname; if (fs::exists(resPath)) return resPath; resPath = fs::path("data") / fname; if (fs::exists(resPath)) return resPath; // Uh, let's just give up return fs::path("/usr") / "share" / "appstream" / fname; } bool existsAndIsDir(const std::string &path) { return fs::exists(path) && fs::is_directory(path); } std::vector stringArrayToByteArray(const std::vector &strArray) { std::vector result; result.reserve(strArray.size() * 2); // make a guess, we will likely need much more space for (const auto &s : strArray) { const auto *data = reinterpret_cast(s.data()); result.insert(result.end(), data, data + s.size()); } return result; } bool isRemote(const std::string &uri) { static const std::regex uriRegex(R"(^(https?|ftps?)://)"); return std::regex_search(uri, uriRegex); } static std::vector splitLines(const std::string &text) { std::vector lines; std::stringstream ss(text); std::string line; while (std::getline(ss, line)) lines.push_back(line); return lines; } std::vector getTextFileContents(const std::string &path, std::uint32_t maxTryCount, Downloader *downloader) { if (isRemote(path)) { Downloader *dl = downloader; if (dl == nullptr) dl = &Downloader::get(); return dl->downloadTextLines(path, maxTryCount); } else { if (!fs::exists(path)) throw std::runtime_error(std::format("No such file '{}'", path)); std::ifstream file(path); if (!file.is_open()) throw std::runtime_error(std::format("Failed to open file '{}'", path)); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); return splitLines(content); } } std::vector getFileContents(const std::string &path, std::uint32_t maxTryCount, Downloader *downloader) { if (isRemote(path)) { Downloader *dl = downloader; if (dl == nullptr) dl = &Downloader::get(); return dl->download(path, maxTryCount); } else { if (!fs::exists(path)) throw std::runtime_error(std::format("No such file '{}'", path)); std::ifstream file(path, std::ios::binary); if (!file.is_open()) throw std::runtime_error(std::format("Failed to open file '{}'", path)); std::vector data; file.seekg(0, std::ios::end); data.resize(file.tellg()); file.seekg(0, std::ios::beg); file.read(reinterpret_cast(data.data()), data.size()); return data; } } fs::path getTestSamplesDir() { auto path = fs::path(__FILE__).parent_path().parent_path() / "tests" / "samples"; return path; } std::optional componentGetRawIcon(AsComponent *cpt) { AsIcon *iconLocal = nullptr; GPtrArray *iconsArr = as_component_get_icons(cpt); for (guint i = 0; i < iconsArr->len; i++) { AsIcon *icon = AS_ICON(g_ptr_array_index(iconsArr, i)); if (as_icon_get_kind(icon) == AS_ICON_KIND_STOCK) return icon; if (as_icon_get_kind(icon) == AS_ICON_KIND_LOCAL) iconLocal = icon; } // only return local icon if we had no stock icon if (iconLocal) return iconLocal; return std::nullopt; } std::string filenameFromURI(const std::string &uri) { fs::path path(uri); std::string bname = path.filename().string(); auto qInd = bname.find('?'); if (qInd != std::string::npos) bname = bname.substr(0, qInd); auto hInd = bname.find('#'); if (hInd != std::string::npos) bname = bname.substr(0, hInd); return bname; } std::string escapeXml(const std::string &s) noexcept { g_autofree gchar *escapedStr = g_markup_escape_text(s.c_str(), (ssize_t)s.size()); std::string result(escapedStr); return result; } std::string sanitizeUtf8(const std::string &s) noexcept { const auto *p = reinterpret_cast(s.data()); int32_t i = 0; const int32_t n = static_cast(s.size()); std::string out; out.reserve(s.size()); auto append_utf8 = [&out](UChar32 c) { if (c <= 0x7F) { out.push_back(static_cast(c)); } else if (c <= 0x7FF) { out.push_back(static_cast(0xC0 | (c >> 6))); out.push_back(static_cast(0x80 | (c & 0x3F))); } else if (c <= 0xFFFF) { out.push_back(static_cast(0xE0 | (c >> 12))); out.push_back(static_cast(0x80 | ((c >> 6) & 0x3F))); out.push_back(static_cast(0x80 | (c & 0x3F))); } else { out.push_back(static_cast(0xF0 | (c >> 18))); out.push_back(static_cast(0x80 | ((c >> 12) & 0x3F))); out.push_back(static_cast(0x80 | ((c >> 6) & 0x3F))); out.push_back(static_cast(0x80 | (c & 0x3F))); } }; while (i < n) { UChar32 c; U8_NEXT(p, i, n, c); // safe; advances i if (c < 0) { // Ill-formed sequence: skip the offending byte (U8_NEXT already advanced). continue; } // Drop control chars except HT (0x09), LF (0x0A), CR (0x0D). // This covers both C0 (<0x20) and C1 (0x7F..0x9F). if (((c < 0x20) || (c == 0x7F) || (c >= 0x80 && c <= 0x9F)) && c != 0x09 && c != 0x0A && c != 0x0D) { continue; } append_utf8(c); } return out; } std::string toLower(const std::string &s) { std::string out; out.resize(s.size()); std::ranges::transform(s, out.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return out; } std::string rtrimString(const std::string &s) { std::string result = s; result.erase( std::find_if( result.rbegin(), result.rend(), [](unsigned char ch) { return !std::isspace(ch); }) .base(), result.end()); return result; } std::string trimString(std::string_view s) noexcept { const char *b = s.data(); const char *e = b + s.size(); auto is_space = [](unsigned char c) constexpr noexcept { return c == ' ' || (c >= '\t' && c <= '\r'); }; while (b != e && is_space(static_cast(*b))) ++b; while (e != b && is_space(static_cast(e[-1]))) --e; return std::string(b, e); } std::string joinStrings(const std::vector &strings, const std::string &delimiter) { if (strings.empty()) return ""; if (strings.size() == 1) return strings[0]; std::string result = strings[0]; for (size_t i = 1; i < strings.size(); ++i) { result += delimiter + strings[i]; } return result; } std::vector splitString(const std::string &s, char delimiter) { std::vector result; std::stringstream ss(s); std::string item; while (std::getline(ss, item, delimiter)) { result.push_back(item); } return result; } bool dirEmpty(const std::string &dir) { if (!fs::exists(dir)) return true; std::error_code ec; auto iter = fs::directory_iterator(dir, ec); if (ec) { // If we can't read the directory (e.g., permission denied), // we consider it non-empty to be safe return false; } return iter == fs::directory_iterator{}; } } // namespace Utils } // namespace ASGenerator appstream-generator-0.10.1/src/utils.h000066400000000000000000000202721506754475600176770ustar00rootroot00000000000000/* * Copyright (C) 2016-2022 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include #include "downloader.h" namespace ASGenerator { inline constexpr std::size_t GENERIC_BUFFER_SIZE = 8192; namespace fs = std::filesystem; /** * Structure representing image dimensions and scale factor. */ struct ImageSize { uint32_t width; uint32_t height; uint32_t scale; /** * Constructor with width, height, and scale. */ constexpr ImageSize(std::uint32_t w, std::uint32_t h, std::uint32_t s) : width(w), height(h), scale(s) { } /** * Constructor with width and height (scale = 1). */ constexpr ImageSize(std::uint32_t w, std::uint32_t h) : width(w), height(h), scale(1) { } /** * Constructor with size (square image, scale = 1). */ explicit constexpr ImageSize(std::uint32_t s) : width(s), height(s), scale(1) { } /** * Constructor with size (square image, scale = 1). */ explicit constexpr ImageSize() : width(0), height(0), scale(1) { } /** * Constructor from string representation (e.g., "64x64" or "64x64@2"). */ explicit ImageSize(const std::string &str); /** * Convert to string representation. */ std::string toString() const; /** * Convert to integer (larger dimension * scale). */ std::uint32_t toInt() const; /** * Three-way comparison operator for natural sorting. * Compares width first, then scale if widths are equal. */ // clang-format off std::strong_ordering operator<=> (const ImageSize &other) const { if (auto cmp = width <=> other.width; cmp != 0) return cmp; return scale <=> other.scale; } /** * Equality comparison operator. */ bool operator==(const ImageSize &other) const = default; // clang-format on }; namespace Utils { /** * Generate a random alphanumeric string. */ std::string randomString(std::uint32_t len); /** * Check if the locale is a valid locale which we want to include * in the resulting metadata. Some locales added just for testing * by upstreams should be filtered out. */ bool localeValid(const std::string &locale); /** * Check if the given string is a top-level domain name. * The TLD list of AppStream is incomplete, but it will * cover 99% of all cases. * (in a short check on Debian, it covered all TLDs in use there) */ bool isTopLevelDomain(const std::string &value); /** * Get the component-id back from a global component-id. */ std::optional getCidFromGlobalID(const std::string &gcid); /** * Create a hard link between two files. */ void hardlink(const fs::path &srcPath, const fs::path &destPath); /** * Copy a single file, handling symlinks and overwriting existing files safely. * * @param srcPath Source file path. * @param destPath Destination file path. * @param useHardlinks Use hardlinks instead of copying files. * @param followSymlinks Follow symbolic links or copy them as-is. */ void copyFile(const fs::path &srcPath, const fs::path &destPath, bool useHardlinks = false, bool followSymlinks = true); /** * Copy a directory. * This function safely overwrites existing files at the destination. * * @param srcDir Source directory to copy. * @param destDir Path to the destination directory. * @param useHardlinks Use hardlinks instead of copying files. * @param followSymlinks Follow symbolic links or copy them as-is. */ void copyDir( const std::string &srcDir, const std::string &destDir, bool useHardlinks = false, bool followSymlinks = true); fs::path getExecutableDir(); /** * Get full path for an AppStream generator data file. */ fs::path getDataPath(const std::string &fname); /** * Check if a path exists and is a directory. */ bool existsAndIsDir(const std::string &path); /** * Convert a string array into a byte array. */ std::vector stringArrayToByteArray(const std::vector &strArray); /** * Check if string contains a remote URI. */ bool isRemote(const std::string &uri); /** * Download or open `path` and return it as a string array. * * @param path The path to access. * @param maxTryCount Maximum number of retry attempts. * @param downloader Downloader instance (can be null). * @return The data if successful. */ std::vector getTextFileContents( const std::string &path, std::uint32_t maxTryCount = 4, Downloader *downloader = nullptr); /** * Download or open `path` and return it as a byte array. * * @param path The path to access. * @param maxTryCount Maximum number of retry attempts. * @param downloader Downloader instance (can be null). * @return The data if successful. */ std::vector getFileContents( const std::string &path, std::uint32_t maxTryCount = 4, Downloader *downloader = nullptr); /** * Get path of the directory with test samples. */ fs::path getTestSamplesDir(); /** * Return a suitable, "raw" icon name (either a stock icon name or local icon) * for this component that can be processed further by the generator. * Return null if this component does not have a suitable icon. */ std::optional componentGetRawIcon(AsComponent *cpt); /** * Extract filename from URI, removing query parameters and fragments. */ std::string filenameFromURI(const std::string &uri); /** * Escape XML special characters in a string. * * @param s The string to escape. * @return The escaped string. */ [[nodiscard]] std::string escapeXml(const std::string &s) noexcept; /** * Sanitize a string to ensure it is valid UTF-8. * * @param s The string to sanitize. * @return The sanitized string with invalid UTF-8 characters removed. */ [[nodiscard]] std::string sanitizeUtf8(const std::string &s) noexcept; /** * Convert a string to lowercase. * * @param s The string to convert to lowercase. */ [[nodiscard]] std::string toLower(const std::string &s); /** * Trim whitespace from the right end of a string. * * @param s The string to trim. */ [[nodiscard]] std::string rtrimString(const std::string &s); /** * Trim whitespace from both ends of a string. * * @param s The string to trim. */ [[nodiscard]] std::string trimString(std::string_view s) noexcept; /** * Join a vector of strings with a delimiter. * * @param strings The strings to join. * @param delimiter The delimiter to use. */ [[nodiscard]] std::string joinStrings(const std::vector &strings, const std::string &delimiter); /** * Split a string by a delimiter character. * * @param s The string to split. * @param delimiter The delimiter character. */ [[nodiscard]] std::vector splitString(const std::string &s, char delimiter); /** * Check if directory is empty */ [[nodiscard]] bool dirEmpty(const std::string &dir); } // namespace Utils } // namespace ASGenerator // Hash function for ImageSize to use in std::unordered_map template<> struct std::hash { std::size_t operator()(const ASGenerator::ImageSize &size) const noexcept { std::size_t h1 = std::hash{}(size.width); std::size_t h2 = std::hash{}(size.height); std::size_t h3 = std::hash{}(size.scale); return h1 ^ (h2 << 1) ^ (h3 << 2); } }; appstream-generator-0.10.1/src/yaml-utils.cpp000066400000000000000000000105071506754475600211720ustar00rootroot00000000000000/* * Copyright (C) 2018-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "defines.h" #include "yaml-utils.h" #include #include namespace ASGenerator { namespace Yaml { /** * Parse a YAML/JSON document from a string. * @param yamlData The YAML/JSON data as a string. * @param forceJson If true, only allow JSON input. * @return The parsed fy_document object. * @throws std::runtime_error if parsing fails. */ YDocumentPtr parseDocument(const std::string &yamlData, bool forceJson) { fy_parse_cfg cfg = {}; if (forceJson) cfg.flags = FYPCF_JSON_FORCE; else cfg.flags = FYPCF_DEFAULT_VERSION_1_2; auto parser = fy_parser_create(&cfg); if (!parser) throw std::runtime_error("Failed to create YAML parser"); if (fy_parser_set_string(parser, yamlData.c_str(), yamlData.length()) != 0) { fy_parser_destroy(parser); throw std::runtime_error("Failed to set JSON/YAML parser input"); } auto doc = fy_parse_load_document(parser); fy_parser_destroy(parser); if (!doc) throw std::runtime_error("Failed to parse JSON/YAML document"); return YDocumentPtr(doc, fy_document_destroy); } fy_node *documentRoot(YDocumentPtr &doc) { return doc ? fy_document_root(doc.get()) : nullptr; } std::string nodeStrValue(fy_node *node, std::string defaultValue) { if (!node || fy_node_get_type(node) != FYNT_SCALAR) return defaultValue; size_t len = 0; const char *value = fy_node_get_scalar(node, &len); return value ? std::string(value, len) : std::move(defaultValue); } int64_t nodeIntValue(fy_node *node, int64_t defaultValue) { if (!node || fy_node_get_type(node) != FYNT_SCALAR) return defaultValue; size_t len = 0; const char *value = fy_node_get_scalar(node, &len); if (!value) return defaultValue; try { return std::stoll(value); } catch (...) { return defaultValue; } } bool nodeBoolValue(fy_node *node, bool defaultValue) { if (!node || fy_node_get_type(node) != FYNT_SCALAR) return defaultValue; const char *value = fy_node_get_scalar(node, nullptr); if (!value) return defaultValue; std::string strValue(value); return strValue == "true" || strValue == "1" || strValue == "yes"; } std::vector nodeArrayValues(fy_node *node) { std::vector result; if (!node || fy_node_get_type(node) != FYNT_SEQUENCE) return result; fy_node *item; void *iter = nullptr; while ((item = fy_node_sequence_iterate(node, &iter)) != nullptr) { auto value = nodeStrValue(item); if (!value.empty()) result.push_back(std::move(value)); } return result; } fy_node *nodeByKey(fy_node *mapping, const std::string &key) { if (!mapping || fy_node_get_type(mapping) != FYNT_MAPPING) return nullptr; fy_node_pair *pair; void *iter = nullptr; while ((pair = fy_node_mapping_iterate(mapping, &iter)) != nullptr) { auto keyNode = fy_node_pair_key(pair); auto keyValue = nodeStrValue(keyNode); if (keyValue == key) return fy_node_pair_value(pair); } return nullptr; } YDocumentPtr createDocument() { auto doc = fy_document_create(nullptr); return YDocumentPtr(doc, fy_document_destroy); } std::string libfyamlVersion() noexcept { const auto versionRaw = fy_library_version(); if (versionRaw == nullptr) return {}; std::string version(versionRaw); if (version == "UNKNOWN") return {}; return version; } } // namespace Yaml } // namespace ASGenerator appstream-generator-0.10.1/src/yaml-utils.h000066400000000000000000000030741506754475600206400ustar00rootroot00000000000000/* * Copyright (C) 2018-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include namespace ASGenerator { namespace Yaml { using YDocumentPtr = std::unique_ptr; YDocumentPtr parseDocument(const std::string &yamlData, bool forceJson = false); fy_node *documentRoot(YDocumentPtr &doc); std::string nodeStrValue(fy_node *node, std::string defaultValue = {}); int64_t nodeIntValue(fy_node *node, int64_t defaultValue = 0); bool nodeBoolValue(fy_node *node, bool defaultValue = false); std::vector nodeArrayValues(fy_node *node); fy_node *nodeByKey(fy_node *mapping, const std::string &key); YDocumentPtr createDocument(); std::string libfyamlVersion() noexcept; } // namespace Yaml } // namespace ASGenerator appstream-generator-0.10.1/src/zarchive.cpp000066400000000000000000000543361506754475600207150ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #include "zarchive.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #include "logging.h" namespace ASGenerator { /** * Chunk size for reading data from the archive. */ constexpr size_t DEFAULT_BLOCK_SIZE = 65536; /** * Size threshold for full extraction of the archive to a temporary directory. * If the archive is larger than this, it will be extracted to a temporary directory * for better performance on repeated reads. */ constexpr size_t FULL_EXTRACTION_SIZE_THRESHOLD = 24 * 1024 * 1024; // 24MB using ArchivePtr = std::unique_ptr; static std::string getArchiveErrorMessage(archive *ar) { const char *err = archive_error_string(ar); return err ? std::string(err) : std::string(); } static std::string readArchiveData(archive *ar, const std::string &name = "") { archive_entry *ae = nullptr; int ret; std::vector buffer(GENERIC_BUFFER_SIZE); std::string data; ret = archive_read_next_header(ar, &ae); if (ret == ARCHIVE_EOF) return data; if (ret != ARCHIVE_OK) { if (name.empty()) throw std::runtime_error( std::format("Unable to read header of compressed data: {}", getArchiveErrorMessage(ar))); else throw std::runtime_error( std::format("Unable to read header of compressed file '{}': {}", name, getArchiveErrorMessage(ar))); } while (true) { const ssize_t size = archive_read_data(ar, buffer.data(), buffer.size()); if (size < 0) { if (name.empty()) throw std::runtime_error(std::format("Failed to read compressed data: {}", getArchiveErrorMessage(ar))); else throw std::runtime_error( std::format("Failed to read data from '{}': {}", name, getArchiveErrorMessage(ar))); } if (size == 0) break; data.append(buffer.data(), size); } return data; } std::string decompressFile(const std::string &fname) { ArchivePtr ar(archive_read_new(), archive_read_free); if (!ar) throw std::runtime_error("Failed to create archive object"); archive_read_support_format_raw(ar.get()); archive_read_support_format_empty(ar.get()); archive_read_support_filter_all(ar.get()); int ret = archive_read_open_filename(ar.get(), fname.c_str(), DEFAULT_BLOCK_SIZE); if (ret != ARCHIVE_OK) { int ret_errno = archive_errno(ar.get()); throw std::runtime_error(std::format( "Unable to open compressed file '{}': {}. error: {}", fname, getArchiveErrorMessage(ar.get()), std::strerror(ret_errno))); } return readArchiveData(ar.get(), fname); } std::string decompressData(const std::vector &data) { ArchivePtr ar(archive_read_new(), archive_read_free); if (!ar) throw std::runtime_error("Failed to create archive object"); archive_read_support_filter_all(ar.get()); archive_read_support_format_empty(ar.get()); archive_read_support_format_raw(ar.get()); int ret = archive_read_open_memory(ar.get(), (void *)data.data(), data.size()); if (ret != ARCHIVE_OK) throw std::runtime_error(std::format("Unable to open compressed data: {}", getArchiveErrorMessage(ar.get()))); return readArchiveData(ar.get()); } void ArchiveDecompressor::open(const std::string &fname, const fs::path &tmpDir) { m_archiveFname = fname; m_isExtractedToTmp = false; m_tmpDir = tmpDir; if (m_tmpDir.empty()) m_tmpDir = fs::temp_directory_path() / std::format("zarchive-{}", Utils::randomString(8)); // Check if archive is larger than threshold, only then use the temp extraction method m_canExtractToTmp = getArchiveSize() >= FULL_EXTRACTION_SIZE_THRESHOLD; } ArchiveDecompressor::~ArchiveDecompressor() { try { close(); } catch (const std::exception &e) { std::cerr << "**BUG**: Unexpected exception when deleting ArchiveDecompressor: " << e.what() << std::endl; } } bool ArchiveDecompressor::isOpen() const { return !m_archiveFname.empty(); } void ArchiveDecompressor::close() { m_archiveFname.clear(); cleanupTempDirectory(); } void ArchiveDecompressor::setOptimizeRepeatedReads(bool enable) { m_optimizeRepeatedReads = enable; } size_t ArchiveDecompressor::getArchiveSize() const { if (m_archiveFname.empty()) return 0; try { return fs::file_size(m_archiveFname); } catch (const fs::filesystem_error &) { return 0; } } bool ArchiveDecompressor::tmpExtractIfPossible() { if (m_isExtractedToTmp) return true; if (!m_canExtractToTmp || !m_optimizeRepeatedReads) return false; // Create extraction directory if (!fs::exists(m_tmpDir)) { fs::create_directories(m_tmpDir); m_tmpDirOwned = true; } // Extract archive fully const auto p = fs::relative(m_archiveFname, m_tmpDir); logDebug( "Extracting archive '{}' to temporary directory '{}'", (p.parent_path().filename() / p.filename()).string(), m_tmpDir.string()); extractArchive(m_tmpDir); m_isExtractedToTmp = true; return true; } void ArchiveDecompressor::cleanupTempDirectory() { if (m_tmpDirOwned && fs::exists(m_tmpDir)) { try { fs::remove_all(m_tmpDir); } catch (const fs::filesystem_error &e) { logError("Failed to cleanup temporary directory '{}': {}", m_tmpDir.string(), e.what()); } m_tmpDirOwned = false; } m_tmpDir.clear(); } bool ArchiveDecompressor::pathMatches(const std::string &path1, const std::string &path2) const { if (path1 == path2) return true; const auto abs1 = (fs::path("/") / path1).lexically_normal(); const auto abs2 = (fs::path("/") / path2).lexically_normal(); return abs1 == abs2; } std::vector ArchiveDecompressor::readEntry(archive *ar) { const void *buff = nullptr; size_t size = 0; int64_t offset = 0; std::vector result; while (archive_read_data_block(ar, &buff, &size, &offset) == ARCHIVE_OK) { const auto ptr = static_cast(buff); result.insert(result.end(), ptr, ptr + size); } return result; } void ArchiveDecompressor::extractEntryTo(archive *ar, const std::string &fname) { const void *buff = nullptr; size_t size = 0; int64_t offset = 0; int64_t output_offset = 0; // Ensure parent directory exists before opening the file fs::create_directories(fs::path(fname).parent_path()); std::ofstream f(fname, std::ios::binary); if (!f) throw std::runtime_error(std::format("Failed to open file for writing: {}", fname)); while (archive_read_data_block(ar, &buff, &size, &offset) == ARCHIVE_OK) { if (offset > output_offset) { f.seekp(offset - output_offset, std::ios::cur); output_offset = offset; } while (size > 0) { auto bytes_to_write = size; if (bytes_to_write > DEFAULT_BLOCK_SIZE) bytes_to_write = DEFAULT_BLOCK_SIZE; f.write(static_cast(buff), bytes_to_write); output_offset += bytes_to_write; if (bytes_to_write > size) break; size -= bytes_to_write; } } } archive *ArchiveDecompressor::openArchive() { archive *ar = archive_read_new(); archive_read_support_filter_all(ar); archive_read_support_format_all(ar); int ret = archive_read_open_filename(ar, m_archiveFname.c_str(), DEFAULT_BLOCK_SIZE); if (ret != ARCHIVE_OK) { int ret_errno = archive_errno(ar); throw std::runtime_error(std::format( "Unable to open compressed file '{}': {}. error: {}", m_archiveFname, getArchiveErrorMessage(ar), std::strerror(ret_errno))); } return ar; } bool ArchiveDecompressor::extractFileTo(const std::string &fname, const std::string &fdest) { // Try optimization: if fully extracted, copy from filesystem if (tmpExtractIfPossible()) { fs::path extractedPath = m_tmpDir / fs::path(fname).relative_path(); try { if (!fs::exists(extractedPath)) return false; // File not found in archive // Copy the file from the extracted location to destination fs::copy_file(extractedPath, fdest, fs::copy_options::overwrite_existing); return true; } catch (const fs::filesystem_error &e) { logError("Failed to copy extracted file '{}' to '{}': {}", extractedPath.string(), fdest, e.what()); return false; } } // Read from the archive file directly archive_entry *en = nullptr; ArchivePtr ar(openArchive(), archive_read_free); while (archive_read_next_header(ar.get(), &en) == ARCHIVE_OK) { std::string pathname = archive_entry_pathname(en); if (pathMatches(fname, pathname)) { extractEntryTo(ar.get(), fdest); return true; } else { archive_read_data_skip(ar.get()); } } return false; } void ArchiveDecompressor::extractArchive(const std::string &dest) { if (!fs::is_directory(dest)) throw std::runtime_error(std::format("Destination is not a directory: {}", dest)); archive_entry *en = nullptr; ArchivePtr ar(openArchive(), archive_read_free); while (archive_read_next_header(ar.get(), &en) == ARCHIVE_OK) { std::string pathname = fs::path(dest) / archive_entry_pathname(en); auto filetype = archive_entry_filetype(en); if (filetype == AE_IFDIR) { if (!fs::exists(pathname)) fs::create_directory(pathname); continue; } // Faithfully extract any hardlinks if (const char *hardlinkTarget = archive_entry_hardlink(en)) { fs::path targetPath = fs::path(dest) / hardlinkTarget; fs::path linkPath = fs::path(pathname).lexically_normal(); fs::create_directories(linkPath.parent_path()); try { fs::create_hard_link(targetPath, linkPath); } catch (const fs::filesystem_error &e) { logError( "Failed to create hardlink '{}' -> '{}': {}", linkPath.string(), targetPath.string(), e.what()); } continue; } if (filetype == AE_IFREG) { extractEntryTo(ar.get(), pathname); } else if (filetype == AE_IFLNK) { // Handle symbolic links const char *linkTarget = archive_entry_symlink(en); if (linkTarget) { // Ensure parent directory exists fs::create_directories(fs::path(pathname).parent_path()); try { // Create symbolic link fs::create_symlink(linkTarget, pathname); } catch (const fs::filesystem_error &e) { logError("Failed to create symlink '{}' -> '{}': {}", pathname, linkTarget, e.what()); } } } } } std::vector ArchiveDecompressor::readData(const std::string &fname) { // Try optimization: if fully extracted, read from filesystem if (tmpExtractIfPossible()) { fs::path extractedPath = m_tmpDir / fs::path(fname).relative_path(); try { std::ifstream file(extractedPath, std::ios::binary); if (!file) throw std::runtime_error(std::format("Failed to open extracted file: {}", extractedPath.string())); std::vector data((std::istreambuf_iterator(file)), std::istreambuf_iterator()); return data; } catch (const std::exception &e) { throw std::runtime_error(std::format("File '{}' was not found in the archive: {}", fname, e.what())); } } // If we are here, we jump to the right file in the archive directly archive_entry *en = nullptr; ArchivePtr ar(openArchive(), archive_read_free); while (archive_read_next_header(ar.get(), &en) == ARCHIVE_OK) { std::string pathname = archive_entry_pathname(en); if (pathMatches(fname, pathname)) { auto filetype = archive_entry_filetype(en); if (filetype == AE_IFDIR) { /* we don't extract directories explicitly */ throw std::runtime_error(std::format("Path '{}' is a directory and can not be extracted.", fname)); } /* check if we are dealing with a symlink */ if (filetype == AE_IFLNK) { // Symlink: try to resolve and read target const char *linkTarget = archive_entry_symlink(en); if (!linkTarget) throw std::runtime_error( std::format("Unable to read destination of symbolic link for '{}' .", fname)); std::string linkTargetStr(linkTarget); if (!fs::path(linkTargetStr).is_absolute()) linkTargetStr = (fs::path(fname).parent_path() / linkTargetStr).string(); try { return readData(linkTargetStr); } catch (const std::exception &e) { logError("Unable to read destination data of symlink in archive: {}", e.what()); return {}; } } // Support reading hardlink regular entries if (archive_entry_size(en) == 0) { const char *hardlinkTarget = archive_entry_hardlink(en); if (hardlinkTarget) { std::string hardlinkTargetStr(hardlinkTarget); try { return readData(hardlinkTargetStr); } catch (const std::exception &e) { logError("Unable to read data of hardlink target in archive: {}", e.what()); return {}; } } return {}; } if (filetype != AE_IFREG) { // we really don't want to extract special files from a tarball - usually, those shouldn't // be present anyway. // This should probably be an error, but return nothing for now. logError("Tried to extract non-regular file '{}' from the archive", fname); return {}; } return readEntry(ar.get()); } else { archive_read_data_skip(ar.get()); } } throw std::runtime_error(std::format("File '{}' was not found in the archive.", fname)); } std::vector ArchiveDecompressor::extractFilesByRegex(const std::regex &re, const std::string &destdir) { archive_entry *en = nullptr; std::vector matches; ArchivePtr ar(openArchive(), archive_read_free); while (archive_read_next_header(ar.get(), &en) == ARCHIVE_OK) { std::string pathname = archive_entry_pathname(en); if (std::regex_search(pathname, re)) { std::string fdest = (fs::path(destdir) / fs::path(pathname).filename()).string(); extractEntryTo(ar.get(), fdest); matches.push_back(std::move(fdest)); } else { archive_read_data_skip(ar.get()); } } return matches; } std::vector ArchiveDecompressor::readContents() { archive_entry *en = nullptr; std::vector contents; ArchivePtr ar(openArchive(), archive_read_free); while (archive_read_next_header(ar.get(), &en) == ARCHIVE_OK) { std::string pathname = archive_entry_pathname(en); // ignore directories if (!pathname.empty() && pathname.back() == '/') continue; contents.push_back(fs::weakly_canonical(fs::path("/") / pathname).string()); } return contents; } /** * Returns a generator to iterate over the contents of this tarball. */ std::generator ArchiveDecompressor::read() { archive_entry *en = nullptr; ArchivePtr ar(openArchive(), archive_read_free); while (archive_read_next_header(ar.get(), &en) == ARCHIVE_OK) { std::string pathname = archive_entry_pathname(en); // ignore directories if (!pathname.empty() && pathname.back() == '/') continue; ArchiveEntry entry; entry.fname = fs::weakly_canonical(fs::path("/") / pathname).string(); auto filetype = archive_entry_filetype(en); // check if we are dealing with a symlink if (filetype == AE_IFLNK) { const char *linkTarget = archive_entry_symlink(en); if (linkTarget == nullptr) throw std::runtime_error( std::format("Unable to read destination of symbolic link for '{}'.", entry.fname)); // we cheat here and set the link target as data // TODO: Proper handling of symlinks, e.g. by adding a filetype property to ArchiveEntry. entry.data = std::vector(linkTarget, linkTarget + std::strlen(linkTarget)); co_yield entry; continue; } if (filetype != AE_IFREG) { co_yield entry; continue; } entry.data = readEntry(ar.get()); co_yield entry; } } /** * Save data to a compressed file. * * Params: * data = The data to save. * fname = The filename the data should be saved to. * atype = The archive type (GZ or XZ). */ void compressAndSave(const std::vector &data, const std::string &fname, ArchiveType atype) { ArchivePtr ar(archive_write_new(), archive_write_free); archive_write_set_format_raw(ar.get()); if (atype == ArchiveType::GZIP) { archive_write_add_filter_gzip(ar.get()); archive_write_set_filter_option(ar.get(), "gzip", "timestamp", nullptr); } else if (atype == ArchiveType::ZSTD) { archive_write_add_filter_zstd(ar.get()); } else { archive_write_add_filter_xz(ar.get()); } // don't write to the new file directly, we create a temporary file and // rename it when we successfully saved the data. std::string tmpFname = std::format("{}.new", fname); int ret = archive_write_open_filename(ar.get(), tmpFname.c_str()); if (ret != ARCHIVE_OK) throw std::runtime_error( std::format("Unable to open file '{}' : {}", tmpFname, getArchiveErrorMessage(ar.get()))); std::unique_ptr entry(archive_entry_new(), archive_entry_free); archive_entry_set_filetype(entry.get(), AE_IFREG); archive_entry_set_size(entry.get(), data.size()); archive_write_header(ar.get(), entry.get()); archive_write_data(ar.get(), data.data(), data.size()); archive_write_close(ar.get()); // delete old file if it exists if (fs::exists(fname)) fs::remove(fname); // rename temporary file to actual file fs::rename(tmpFname, fname); } ArchiveCompressor::ArchiveCompressor(ArchiveType type) { ar = archive_write_new(); if (type == ArchiveType::GZIP) { archive_write_add_filter_gzip(ar); archive_write_set_filter_option(ar, "gzip", "timestamp", nullptr); } else if (type == ArchiveType::ZSTD) { archive_write_add_filter_zstd(ar); } else { archive_write_add_filter_xz(ar); } archive_write_set_format_pax_restricted(ar); closed = true; } ArchiveCompressor::~ArchiveCompressor() { close(); if (ar) archive_write_free(ar); } void ArchiveCompressor::open(const std::string &fname) { archiveFname = fname; int ret = archive_write_open_filename(ar, fname.c_str()); if (ret != ARCHIVE_OK) throw std::runtime_error(std::format("Unable to open file '{}' : {}", fname, getArchiveErrorMessage(ar))); closed = false; } bool ArchiveCompressor::isOpen() const { return !closed; } void ArchiveCompressor::close() { if (closed) return; archive_write_close(ar); closed = true; } void ArchiveCompressor::addFile(const std::string &fname, const std::optional &dest) { if (!fs::exists(fname)) throw std::runtime_error(std::format("File does not exist: {}", fname)); std::unique_ptr entry(archive_entry_new(), archive_entry_free); std::string destName = dest ? *dest : fs::path(fname).filename().string(); struct stat st; if (lstat(fname.c_str(), &st) != 0) logWarning("Unable to stat file '{}': {}", fname, std::strerror(errno)); archive_entry_set_pathname(entry.get(), destName.c_str()); archive_entry_set_size(entry.get(), st.st_size); archive_entry_set_filetype(entry.get(), S_IFREG); archive_entry_set_perm(entry.get(), 0755); archive_entry_set_mtime(entry.get(), st.st_mtime, 0); { std::lock_guard lock(m_mutex); std::ifstream f(fname, std::ios::binary); if (!f) throw std::runtime_error(std::format("Failed to open file for reading: {}", fname)); archive_write_header(ar, entry.get()); std::vector buff(GENERIC_BUFFER_SIZE); while (f) { f.read(buff.data(), buff.size()); std::streamsize n = f.gcount(); if (n > 0) archive_write_data(ar, buff.data(), n); } } } } // namespace ASGenerator appstream-generator-0.10.1/src/zarchive.h000066400000000000000000000066271506754475600203620ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ #pragma once #include #include #include #include #include #include #include struct archive; namespace ASGenerator { namespace fs = std::filesystem; enum class ArchiveType { GZIP, XZ, ZSTD }; std::string decompressFile(const std::string &fname); std::string decompressData(const std::vector &data); class ArchiveDecompressor { public: struct ArchiveEntry { std::string fname; std::vector data; }; ArchiveDecompressor() = default; ~ArchiveDecompressor(); void open(const std::string &fname, const fs::path &tmpDir = fs::path()); bool isOpen() const; void close(); /** * If this is set to true, and the archive is large, it will be extracted to a * temporary location and entries read from there. This avoids repeatedly * seeking through the archive to extract data if readData() and extractFileTo() * are used a lot. Repeated seeking is slower than temporary extraction. * * @param enable Enable or disable optimization for repeated reads. */ void setOptimizeRepeatedReads(bool enable); bool extractFileTo(const std::string &fname, const std::string &fdest); void extractArchive(const std::string &dest); std::vector readData(const std::string &fname); std::vector extractFilesByRegex(const std::regex &re, const std::string &destdir); std::vector readContents(); std::generator read(); private: std::string m_archiveFname; fs::path m_tmpDir; bool m_canExtractToTmp = false; bool m_tmpDirOwned = false; bool m_optimizeRepeatedReads = false; bool m_isExtractedToTmp = false; bool pathMatches(const std::string &path1, const std::string &path2) const; std::vector readEntry(struct archive *ar); void extractEntryTo(struct archive *ar, const std::string &fname); struct archive *openArchive(); bool tmpExtractIfPossible(); void cleanupTempDirectory(); size_t getArchiveSize() const; }; void compressAndSave(const std::vector &data, const std::string &fname, ArchiveType atype); class ArchiveCompressor { public: explicit ArchiveCompressor(ArchiveType type); ~ArchiveCompressor(); void open(const std::string &fname); bool isOpen() const; void close(); void addFile(const std::string &fname, const std::optional &dest = std::nullopt); private: std::string archiveFname; struct archive *ar = nullptr; bool closed = true; std::mutex m_mutex; }; } // namespace ASGenerator appstream-generator-0.10.1/tests/000077500000000000000000000000001506754475600167365ustar00rootroot00000000000000appstream-generator-0.10.1/tests/ci/000077500000000000000000000000001506754475600173315ustar00rootroot00000000000000appstream-generator-0.10.1/tests/ci/Dockerfile-debian-stable000066400000000000000000000006361506754475600240200ustar00rootroot00000000000000# # Docker file for AppStream Generator CI tests # FROM debian:trixie # prepare RUN mkdir -p /build/ci/ # install build dependencies COPY install-deps-deb.sh /build/ci/ RUN chmod +x /build/ci/install-deps-deb.sh && /build/ci/install-deps-deb.sh # install 3rd-party stuff COPY ci-install-extern.sh /build/ci/ RUN chmod +x /build/ci/ci-install-extern.sh && /build/ci/ci-install-extern.sh # finish WORKDIR /build appstream-generator-0.10.1/tests/ci/Dockerfile-debian-testing000066400000000000000000000006371506754475600242240ustar00rootroot00000000000000# # Docker file for AppStream Generator CI tests # FROM debian:testing # prepare RUN mkdir -p /build/ci/ # install build dependencies COPY install-deps-deb.sh /build/ci/ RUN chmod +x /build/ci/install-deps-deb.sh && /build/ci/install-deps-deb.sh # install 3rd-party stuff COPY ci-install-extern.sh /build/ci/ RUN chmod +x /build/ci/ci-install-extern.sh && /build/ci/ci-install-extern.sh # finish WORKDIR /build appstream-generator-0.10.1/tests/ci/Dockerfile-fedora-latest000066400000000000000000000006711506754475600240570ustar00rootroot00000000000000# # Docker file for AppStream Generator CI tests # FROM registry.fedoraproject.org/fedora:latest # prepare RUN mkdir -p /build/ci/ # install build dependencies COPY install-deps-rpm.sh /build/ci/ RUN chmod +x /build/ci/install-deps-rpm.sh && /build/ci/install-deps-rpm.sh # install 3rd-party stuff COPY ci-install-extern.sh /build/ci/ RUN chmod +x /build/ci/ci-install-extern.sh && /build/ci/ci-install-extern.sh # finish WORKDIR /build appstream-generator-0.10.1/tests/ci/ci-install-extern.sh000077500000000000000000000011461506754475600232340ustar00rootroot00000000000000#!/bin/sh # # Build & install AppStream Generator dependencies # set -e set -x # # This script is *only* intended to be run in a CI container. # mkdir /tmp/build # build & install the current Git snapshot of AppStream cd /tmp/build && \ git clone --depth=1 https://github.com/ximion/appstream.git mkdir /tmp/build/appstream/build cd /tmp/build/appstream/build && \ meson setup --prefix=/usr \ -Dmaintainer=true \ -Dapt-support=true \ -Dcompose=true \ -Dapidocs=false \ .. cd /tmp/build/appstream/build && \ ninja && ninja install # cleanup rm -rf /tmp/build appstream-generator-0.10.1/tests/ci/install-deps-deb.sh000077500000000000000000000026001506754475600230150ustar00rootroot00000000000000#!/bin/sh # # Install AppStream Generator build dependencies # set -e set -x export DEBIAN_FRONTEND=noninteractive # update caches apt-get update -qq # install build essentials apt-get install -yq \ eatmydata \ build-essential \ gdb \ gcc \ g++ \ git # install dependencies eatmydata apt-get install -yq --no-install-recommends \ meson \ gettext \ gobject-introspection \ gtk-doc-tools \ xsltproc \ docbook-xsl \ docbook-xml \ libgirepository1.0-dev \ libglib2.0-dev \ libstemmer-dev \ libxml2-dev \ libfyaml-dev \ libxmlb-dev \ libcurl4-gnutls-dev \ libsystemd-dev \ gperf \ itstool . /etc/os-release if [ "$ID" = "ubuntu" ]; then catch2_dep="catch2" eatmydata apt-get install -yq --no-install-recommends g++-14 gcc-14 else catch2_dep="libcatch2-dev" fi; eatmydata apt-get install -yq --no-install-recommends \ liblmdb-dev \ libarchive-dev \ libpango1.0-dev \ libtbb-dev \ libbackward-cpp-dev \ libunwind-dev \ $catch2_dep eatmydata apt-get install -yq libglibd-2.0-dev || true eatmydata apt-get install -yq --no-install-recommends \ libgdk-pixbuf-2.0-dev \ librsvg2-dev \ libcairo2-dev \ libfontconfig1-dev \ libpango1.0-dev # install misc stuff eatmydata apt-get install -yq --no-install-recommends \ curl \ gnupg \ ffmpeg \ npm appstream-generator-0.10.1/tests/ci/install-deps-rpm.sh000077500000000000000000000021611506754475600230630ustar00rootroot00000000000000#!/bin/sh # # Install AppStream Generator build dependencies # set -e set -x # update caches dnf makecache # install dependencies dnf --assumeyes --quiet --setopt=install_weak_deps=False install \ curl \ gdb \ gcc \ gcc-c++ \ git-core \ meson \ gettext \ gnupg \ gperf \ docbook-dtds \ docbook-style-xsl \ libasan \ libstemmer-devel \ libubsan \ libunwind-devel \ 'pkgconfig(cairo)' \ 'pkgconfig(freetype2)' \ 'pkgconfig(fontconfig)' \ 'pkgconfig(gdk-pixbuf-2.0)' \ 'pkgconfig(glib-2.0)' \ 'pkgconfig(gobject-2.0)' \ 'pkgconfig(gio-2.0)' \ 'pkgconfig(glibd-2.0)' \ 'pkgconfig(gobject-introspection-1.0)' \ 'pkgconfig(libarchive)' \ 'pkgconfig(libcurl)' \ 'pkgconfig(librsvg-2.0)' \ 'pkgconfig(libxml-2.0)' \ 'pkgconfig(libsystemd)' \ 'pkgconfig(xmlb)' \ 'pkgconfig(lmdb)' \ 'pkgconfig(pango)' \ 'pkgconfig(libfyaml)' \ 'pkgconfig(tbb)' \ 'pkgconfig(catch2)' \ sed \ xmlto \ itstool \ diffutils \ /usr/bin/ffmpeg \ /usr/bin/node \ /usr/bin/xsltproc \ /usr/bin/npm appstream-generator-0.10.1/tests/ci/run-build.sh000077500000000000000000000011001506754475600215610ustar00rootroot00000000000000#!/bin/sh # # This script is supposed to run inside the AppStream Generator container # on the CI system. # set -e export LANG=C.UTF-8 ROOT_DIR=$(pwd) echo "C compiler: $CC" echo "C++ compiler: $CXX" set -v $CXX --version meson --version build_type=debugoptimized with_backward=true if [ "$1" = "codeql" ]; then build_type=debug fi; if [ "$1" = "coverity" ]; then build_type=debug with_backward=false fi; # # Build Project # mkdir -p build && cd build meson setup --buildtype=$build_type \ -Ddownload-js=true \ -Dbackward=$with_backward \ .. ninja appstream-generator-0.10.1/tests/ci/run-tests.sh000077500000000000000000000005001506754475600216270ustar00rootroot00000000000000#!/bin/sh # # This script is supposed to run inside the AppStream Generator container # on the CI system. # set -e export LANG=C.UTF-8 ROOT_DIR=$(pwd) set -v # # Test already built project # cd build # Run tests meson test -v --print-errorlogs # Test install DESTDIR=/tmp/install-ninja ninja install cd $ROOT_DIR appstream-generator-0.10.1/tests/meson.build000066400000000000000000000033471506754475600211070ustar00rootroot00000000000000 # Miscellaneous tests tests_misc = executable( 'tests-misc', 'tests-misc.cpp', dependencies: [catch2_dep, asgen_lib_dep], include_directories: [src_dir], ) test('Miscellaneous', tests_misc) # Database/Cache tests tests_db = executable( 'tests-db', 'tests-db.cpp', dependencies: [catch2_dep, asgen_lib_dep], include_directories: [src_dir], ) test('Databases', tests_db) # Network-dependent tests tests_net = executable( 'tests-net', 'tests-net.cpp', dependencies: [catch2_dep, asgen_lib_dep], include_directories: [src_dir], ) test('Network', tests_net, args: ['--allow-running-no-tests']) # Report generator tests tests_report = executable( 'tests-report', 'tests-report.cpp', dependencies: [catch2_dep, asgen_lib_dep], include_directories: [src_dir], ) test('ReportGenerator', tests_report) # Icon handler tests tests_icons = executable( 'tests-icons', 'tests-icons.cpp', dependencies: [catch2_dep, asgen_lib_dep], include_directories: [src_dir], ) test('IconHandler', tests_icons) # Engine tests tests_engine = executable( 'tests-engine', 'tests-engine.cpp', dependencies: [catch2_dep, asgen_lib_dep], include_directories: [src_dir], ) test('Engine', tests_engine) # Debian backend tests tests_backend_debian = executable( 'tests-backend-debian', 'tests-backend-debian.cpp', dependencies: [catch2_dep, asgen_lib_dep], include_directories: [src_dir], ) test('Backend - Debian', tests_backend_debian) # Debian backend tests tests_backend_misc = executable( 'tests-backend-misc', 'tests-backend-misc.cpp', dependencies: [catch2_dep, asgen_lib_dep], include_directories: [src_dir], ) test('Backend - Misc Tests', tests_backend_misc) appstream-generator-0.10.1/tests/samples/000077500000000000000000000000001506754475600204025ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/appstream-logo.png000066400000000000000000000535061506754475600240530ustar00rootroot00000000000000PNG  IHDRUS pHYs  tIMEtEXtCommentCreated with GIMPWbKGDVIDATx}x6ZRRnwZ܃B$!,Fe}fwn( 6}L;;;3yϹizqSߝјMК-Ƥ?8{/h%ZZk ڗhM+B Z;FshNTa>(m@y-vmZglm;\(fdC u׏ut9a_k~\`XvZmqoKY +7N7"yD֖2H4^ jwA.gh#B[u-6C S?D+ ʒoebJ>!S-:} Í]R%/P xS𴪯)ED[tDkK\K=*i]UOH? "fN};C]\v#0V r RW+eaXrj8Q髓R҉ێP!6'5xAEDkDGeVi@J SQ~-^b1?w`h5"c$1YG`P i32 f#s)=ԁtkwHņ^ {Bqڸx<Bb]|ףPdJ~ Xb,bKG8YX5"qwiоcA#Jio\ѯ nFpCDw.crJt¶@Sk!^ "L7 V2.4>BL#z\df2$HBE+d M|TδoƗ$6j}U( tyrg>* 籉4{u+zo (^KˉOq60+IYH2 9эDBHؒįt2@L2[#7zlO]&:5]:'X2MDH`[_PfRfQP|ZYDBfJ`-OWT|DY(~q )B#_͉`?g2 ̚M[ sXxy{?8"LT aHhQJzڈ$Rp|\'WyM= ],T3-J̬8kĢw 69F/(P)z߃7/ϐzo쌏^;xAxeY"@Lq? )u7^XTǧ >|7thmqwbe k_fn`:-ܻF'Ms|vu`3~$X /b_ݕYZZ0*a]YqĢ bd:~c̞~t[k &gNAƀowOd/kp//ʵ%1&wFpK] /,,7R;ip=o暦w5@]F]j^cXFV&%Xc/9GBsm62$OH䢣əRDsggts1;yxE0j(Wn#]}ptqwww>JA`"$4").CpL$a @3g]{0-~?OΈ|3)Iq-1QayXbT@ɣJ")hbnfW`hMKMI9/4~$4:Q SM2h|F[{ _^rl\|061--)8:2>%-Tr !εɶgq2Ml6b2w37~ 6v`ffo߾@Ett4[ ԏPU%>3S62 iy9v3r~cnYqOFvm2ZŢJ⾍nSo|OX`=)ZPwCu no%8^~wF/; >`}p ΂#`~4’xIw&Pl| Ka1qnF74 l._Ľ|{IB#"|P{X"僁 ۣ;Xڭb6 U޻.BWwMREs_ޱ_=]y͆ώ\'7G APdGa,4T:^ؒw[VoyFuܳP;kEZWDDaV_Qplooncvś0/j͗V]qa7s_A v v50: aπQz"N[,a֐1*#4&㲍WxgcB3HPh ~~{%W?Y:lds|BdDhl)N{icPi|KX$xՉ&{:pgXKtj1?_-)i'Dkt'I:m:,}(FsN;s+z':8݂,ېvj`x<=q!ﮮ¢ҹ4aE7$d2n6Wbc e`kd~Yׇ @d ےA֩MnE7>+az$O]{.$$YFf%R[lAHs-]*Iarse{_3z47?J{A %@?E8]i[/+lB wWjW(ZG<m  {t^ٜ]tD?iIG{ _ۇ'!7<I ,D>Z?)NaaB PA؜_P \,H.7^jX$ciS)"iR1M\,G['c90SA[GArш,Мھ,tIi~eCU(G<1ʮ#Cz15,!hm`k6~)l1㥙뉅u4fr'4ap]?f'!Q~#waʶYln "MYi! e%@y*:P@,օ*N;(o6n#MJ3(("/L+D, SDF9\pLJmE%gi(,TGіDOj~wAԺͷg5)0mn'(i{ )LaKzwmf| ׉ mq +,Τ|q2&•h` )$vuMjS/L{4&@%QVGc9H 22ҹs4ZdsWpTwm+M`)uɘRݏԝȁqo|lωoܣW8asc ۉ`~3zA>нV pYDgcT0JhҞe5 ّOF32**j }DJBGC~m"oLORx<"X9;=>Zd 96bD&~FQ]Y@BɯyѱŷRs$9yʋ!h+M ^߾=&x$y&z΍٬J,`Vx0A 0BiŰMv%>3+smrUL$$E8xkf4ҠdՏ o:;ocf'0r=>,y/3`v5٬}Sz: _w1.G(CEEDGon;;aGlK$^AE%xƌ(R)!ifRN"W(vK4\!!̃Y&f@56&B|ि2=A94Vo?T-\ipԊt583'tT4GMpuz)tvS/`>,mhL}ژ'"I늤p?7sv+T^w[0tMȕ/tx{A/5 H?vR9Oԍɓm˩+Oqn6m*ܵkt˖Et; dSXbs%ODY=} \O#!T+* 5| Ϲx6.*sss{gN`kh#Z!B>Y*//)D(. vҶ29xKO= /qAaxҳ^;gv/7~i&,!@n# M#%42v+A%!1tхwۺ5kܹST_p݂OR ì29$[Y"ͤ~u{uly] a19&EHȀ^ gee 666d!y9yE(ۍ3aw':qnFUI6!rcsօ'mm{aM\ghК u* ܆\;B9%߳ ”bgg[6I/?"14(ֲNN9,"wv{_B-*#)-¬dMWW4o Abbz~~hXחswwYٙ3x)i(P Na46&-U$f tpm;G?(#, R:d??өԢ q'CN[P.LB}/'[d1<# csaql=Ȭgjw$u2 M,SP*}(x7)Lٔϕm {Ń!iXa)CFNg'ȵgYd]LT#A_RK]'Ӑ)q?Q?͕}t=[\Fk?QJAU;4k/d kS(F2%6fհefxTUAe<2r$ /{tB`}K ,C cɰB@Psq)9k\J_ =<A;VCCӳTl^n͟o=Yqa\]>eY\-Voqk-e8^>= !KxPOt>傁S2smlkhyR Faqq555'ݾ}{8jfHB(݅B|hgXMc|]4Dpt3 0PыI %Tڕ ϭ5haitg-mJ2S2JA&$=|"t{F"wiDZ8Z#,ARXB!N͔b @~avq# 60]]Ϙ\8FCY!Dp e.9.ԍcd4PSw8&l0ඔ-S7ԮgneAb;Z7:pDH J5?Шd09$ 4P7{PPg '0Gp'r λ98,Rd><3Hv&="Ұ02Lм :E`xb&vlH+̌L-ft]( T.2((3;e:{YhhTo!oFf,M@=/01L\S$вG6 bs gk;bAnkRC$T{N]i.np- -N7RҙlCKzŪrᅪ*}i¹xB"xIRX[$HHʀ$*&^"2.PSI}xA.4%#yŒ4d kN@!A]#[;XBZFѬ0aj;iNXc)Дѱqё II/ ^oڴYnڵ4nP]K#JYc ]Qo>eYv9eܹ٧]a97pm|T{8o>lS5g DnQoK M{ཱྀ'RË.>rD / ##ox2I}%ߘIRKd qtL07nPŇN8` QуoCe0h}N< `Ķܨ]f0fg 8p;O.JKwsscena9 vYot~ 'Uv6zIK4(8.\ZezZܐ@{ iQaY s3v pgc [9NW:>3c| @bkP̺H$jD]$uL^$/믺wnsVqŘIL83qe_i  Zq@= H6AǏ rW<;f g~À`઻0dc9nr@Fx>7?;z9wI׃IINFRtSbRIPY0PsФD!H6 IhOi..6;UpM7y uWPf%74Qy!wFq~ӣ}Mn,9 KB|Im>-q`(&4«tnJbS5q{-DVkgh.N܍ǶPS H9EbXVA(I,at * ,I)6ZIzlTy w!`!=a"Rx5*-/lL:=x2cR*1%xL|F 'AP\^"xԽ=xf@p+x/${9N6lb$~9v>/'90K$AaQ1>{FVsp>!/3ɵ+|KW 8^~n=5M]s=omv7W6{vܘ-zksrܼ)Qn"R@,pvҋ0r%t+Pg;hڙ2q`Vl-= Zg3 aGx춆?Oz.,E1TJb N\z1@ɒ276;>6hNT]34e?NQ\͉7 5v6Ym'Z R2v%fsc.OXq;褫 s9+;4C2#>p({e'0o̝Y9" @Ar %<(0*cP,C`iλ{lᖁ',p,DlI䥰ҴpVqnN+@Ne$wT']{͛|ԑ3j6dc2"#F eQ 8{ wea4r"": }&R72M$n>.̋{9b 3ao /% |HdSLs$MTr(Zi9RIFn?pNgb$۳NV_x:W,eS\a(eə_% %7x̂\`vSt &0| \ yehg/q*2.0"pF c&izr1 ,$R ! )ɵ xHj"4"dݩ0]JG'P ry᣿..xY샑so,H{Al&$ _( 48@6MW}أK;514LzOtX'^,RrD T+ؐPsLz)$$Bjf Q*b- ]@&,:ö銝V T ݉mwXF5o)@A";ph@L"cYG0KA#"֥cR6pr#a)ʩ9B6^hmYGGc $^yBBװ,$[Hw?GvFZqoF"bag=]`5 *؜3{p3[r \WT~q` ^#RV2pu׽ w  g `[G Hkc 9(⽠(~܃*kT0dn 64;l3fĶ"=DS;Kw/Fd~;퐅x:]v̀OuNOt"҈&Fr``k a 2~KJI^`ƕ6Iۯ;lG%)xM=Aw%!{>/ˁQ[UaڀPP YUad]?ݐ]|S|D'@"630#^(MJElx`12:CZD |E2x{ L0qd[ĉ= /دT eXݖMU`\!c<ІxłKU@Q_ ao\}_ȕ!Fl}-t!oe- u <#sQyҠB]eb\E&䗀@0vb5I-Jw n+r̉UmUyo{=++g {ق;(} fZUqLM~M2N=guljF" ?kyKw =ʌU#0x_~#b[#0E7gj٢& uE]:Mp|]mif<#Bn]z~xl߅/A)i*Fx˞κ gv%w#/##JD!0 0oI^U@Yg ~{AX;/U~_&hi=b}u_S2l;dn"0] 0^1d 7[-0)ޘ:ŻP vLRBq"s( i u(W =54,M<>7pf0pu3O. =oOXx^y/00E lQK.@ P0Huਉ54ʁ^j4#dD2)#@#Sƶ7R-;kX+p`(GD%pԍ9jGAce jEXcm"0Ƚ ]bL\z)7e>h(0A`ȁQ ER0o=ſjje͡{|f*1^ T8|eЮȲ4nQ%<0~1"Z!hs%h3zsl2b˫J{+Ti j&Y :.ZClh%`ެ jFq c|#ksWɥgD2`4m lT!PWR]ۛ@EKAAU0s ű xSwK#}@902SYb20{r`ko0H-BZX:3hmT!sCbhR$@8Z)[yFa-ҏf'^|b]gn\nlQ٪F]QN. y6'9;5! WVw 7F:7R0bsp.=ߦ y[H+4>kP;f&CAj_ e >yӏXx>OA&PДL! %Y6ThH*Bb` zY5C歡i6u#1h>L(̠M ~Y ǧ-t l1`y;FHtA2XCP60T5o zO?jq|DXLް7ls-MB[Rm6A_p%Vk-pV?g-H ~?ri]_~<*2-fe ,;vϿAL9LD0[~>۹ ˒[eYO`(zg5:dG$@7B\ r>@Zf.8>oBњlz(_6!9wTays} ~riSpIJy r~s" |Ӳ'h3,d8.?q}4PjJJЮo0#$ h|:`t\[T`>Nw DkY-y]!V}'h6Z>~o?)G!.ٲ}VZŲ 3Y}h?Fy8em ڌ =3C7uOQp~^wcL<`OV` bBhU+[ $%|Qku`QBwȝf;W݇a>Rq$BK`-gzrhܬe!gGڛA\Wqj d]1uILZA[hº'ѦS2ܚQB} +ɹ Xw'*`(6*i~ 9Q{ED lt/RSBT}U딦-]XٕhF,m@يb6_!~^k_z׫e}L1d{HE u}H4J 7 _Fa9Rsu|HBF Xtzj~8 =sY.u~)-EFYB{Џz[UՔ_q3KF{|ݢ(/8 r`QU30`]\y$,w1%']Qj]V/"05Ջf#k>[ClpI9g> άU7Rg4[&ؠe+8af A+Fv]wXĕ}ߪePi-0И 1-Ȉב )$SĿ}\`l}\8o-A}f} p:[72x3X&i|.04 HbEǮcYOF-,+0BJЇWidb$DAν(AVAka^3~/a]g篱ѥ1^G֨K>Q(}P?)*i6`cXyٍYylWv? ~0{m(,$SrpJ7ق,fvBn mP2U(4iߥ^N?-ˤhGBi& (AP[;u{G^?d{!Tux4E./ANPz/ u)ˆ9+]Kvȅ j^HN>dږ :}ꎨ1Z#AUXSC룺5FDT^fZ֨[u\b,uH\?[ߐ'&AWPpQXpjҼ{zpc$8mP!% '7;$CvrHBiƹg-MY0d8*2sud4&֨:ՃC#rHz9 "@6L)8<\g9IrW:K5 2YF`T^ ZRl[w"{0a!\~7 K%|#Ieb < bKڡZCR bA<:X\s70$Ԅ鬵jOExkh6:u ;ȧkm!H+6"iԬ >ϯ}4Nq &@|Q#g& 7 QY9(u%:Uh-"5>AR]WwQ]lQs噂@D4@?zd+5Lc#  8ߴ -z8 w:aHhI^{ ?~fm:uw &]P!Ih":^H^Ag=[BaCaFP`j#ZruD]z= +`8N>(}4UM|3i[FpT'b[aXu+Jd4mt~T^_-wC8y~(f9:vm'+ԙy1#r`/T-Ȃ!5Yj}5iZt| eTE>[r`8KE. I+y{?A/~;!$IXEȟH@j>eFso _ YɪHF_y`(Ϊ5h#CX0xv`8~jGe7R-V A>v0=(U_/Zh?M[z̈́cQ+epeP;5anSP9DP(`ᬖ 4') 08`HJkvP¶'f^VsU~gNh~hZF1=dKX ,C:\pG#":ċK1u;oࢲZ`:)$f J2iːEp MYD%dO ue8>>PȁQ>M1xxWYIuX%/QP6݃BR>5]tJCYcZ[q s݀T)9B/zT( E_p ׽Aof:VEf!K`QG~JBf)_G>>KnCv_MSKOB[x"{@U`ܨШv,FC4E^Uuoear\Bs`@PQK/"@.SU .4m ?MBI26F_;mYЬumH80|cK[cJļ6L)sGBмgربϑ\tC2tFmxZ'Œh{(M?1cB@TFq%tƍD"ZMB{"(*8{Fޯ0l:"a[yeWF \56?m;ɤ/×w>Z>y,r PċS$#) ~~8nϕ3La_XHLzP̂E(t10Mt7ZVp`)(v(fOi=n}rit!=vp:CJ*L<yK Fju0q)X'YLy8jTņ~Fþ;^(2UVG.q"~v1+9 ;oc5~Zؤ U'B>r)݇΁.Y C, Cܲ%K޸b4cHjTjG"nDJ.Ĉ/a70j1o#=Lmr:),C2_&_+Ն~:0ex2ZK+4Es<WŸ3 QPH'B]XzHz(/S\E04w0nݨnm?@؃,!]S:xjVWM _B#UFu"Lypǭ ʠU"Hf2qDXҐ-EW % "د z.FNfu)+D&(7~`E3e 주VQH֟Aۯ~" i!k/{ Wpo*}q>~? =,Y'Q>*\ ƽJBB:ș:A: SD2˩6кhܾ+f7ˀX ;=Vއ!-WX.d3İ#hԴ9M} ˎ<t_Io\S1J {fq3nT#ikBBQJGks_# h\uOgjbR\G%WS&6 _>÷rD]H1 *'2iz@ס߲pX':J҃:ШI}m UDzcRDD8'AU`Bk5N.C dVylw I R=5dd]y!\zůϭ((K. A+Xds%dM@&HyQ)Ư] #WߪJZv~ 0uX~!7y|Lh,[LBۂ;6nhX愖V}@jwdl%UӜZ3h}bVL5”W@y{QU^/=QAL^CXm[p4%Oq&I-KsB!sƕӱN%W 9].#O$ /xU jx_RY'H3OL9--MBu1@R$תZkF "<L2FVn0c&4W(mGXayZݪ|,/ih߯ e?d )Q%1OQXzIcől+⦐q(vOȽW^:|GEǾDYD{憖E+BOwsH.THe&2R=i~(\w!(yÔ סIvS. y D `[c3l_uSsƱ>ihWў!Hђ(h,\U*_jA};T+Q< EnE7aUGV.b ShMG7;t G&ICc vB{*Ḓ Ba4 N#6D2x"&^*>d>))b;4ϣ],@٣ Pp[k)0lulR,iey% [oLa90,\=5Q֝~6_tMhï>#oǾ _WW^йi^!$-/my10{ Z)P*M*APhW ۚaH< m?:e SK*˼V9~*ܹ=gi{_*[ubXy~zϿX[~pI?ㄝȨbjSH}o^YAdp % 2x ¸۽h"0=(B\C{E4 %  WvGbz-ф#8yi.JsfpVZw:I#!ݿM=#Fas]&?ORĝxE#dJJфrƐ!P1 ݣvwx Shh0O 裳٧nj`V}xd :Z]|8atm r V]79$/s:]ooa<{X +@I,Cآ"K#,_{dF+*L)(1Rڼ RTp)?*ʌn*/3utmno>X?hw|g}sgy?eOO`Xyzf q[V`ǑXy9 '539Fck;0uݣW E5K+6qX+dA&ͪ"A0m=z@-%h@J:$$ÑK@)9y;#,$NzǕ?Tѭm THkյjoLvJ7+Ǿ&Y P Bm{z 76:6fAE/|`9o6yߡP"(]nVinwz % RSJ&R aTZCD3; :a-F-Jl9 9K=D*)iT YlCOd񗫙gj@5=Y; 7(!GB2,G(GZxo ~?qbɛ O᥹MmoWi-mLօ+sF?P{Wh1>SJS!4]|"Z +E8Пy[ f:&aYVުJFJ-'>nɭmI z 8`P IWtI @l?a@Z? bsJƝrcUZ>°P׹`FЧ`O-[U%Q; Y/d@>ShC7dXL\M5KJZV$d =g0v.g{͹@wC#hI誇^ŋMĎ ygNَ^xm*RIENDB`appstream-generator-0.10.1/tests/samples/debian/000077500000000000000000000000001506754475600216245ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/debian/dists/000077500000000000000000000000001506754475600227525ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/debian/dists/chromodoris/000077500000000000000000000000001506754475600253025ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/debian/dists/chromodoris/main/000077500000000000000000000000001506754475600262265ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/debian/dists/chromodoris/main/Contents-amd64.gz000066400000000000000000000216561506754475600313100ustar00rootroot00000000000000jVContents-amd64[6Way]U]$;EY$ CI6%Iʗsx/DRT\<C2RF 72!8+x))uJHR\BȓTJnpIWɡ*ƊU6'K.mz<J%h*d˅,*V6@VHy` $9QDT&NN Z&8 f:@GHJ~[suĖ :mr]ٱ;[׺& n Jr|YqGw B 2sK0cRÂ/?׿|GbЦ>p|`/b-KgK+%7T Cׁk%F!WNɞ0HbIrMÍB"Ҷ>@ eИDzM9/U}baśWU5gr@{nD{HW5>T7h;%0(g U69w;Z;4pR5H֟I@vF=B4!NW_y+\NQ5?*zoKֺSWpLo?|>=,$I-j C  7D-J(ȡN~sBTPF9<8a%ud5 {Ubj2I2=H,czD#7€ 0k6\ -b[z;)qfq@Hk.@A 1SP%~߄'٥%yB`ApśJ #h4䀤A{żu3S䱖2ƉbG'DT˜CQX ^ɥBU#({ۖ4 ᶀ !9XEliʹ*erI ZSO3s5-frސ㶄6߲6X]%g1:! ܥuאqdHQv#pٱhF'i1<:LidEev!H@ RtqIZQPlF01,Y (4-u%ẟ5:$IM *bTnp<\3󅷢,Sf9vV4miG̭WhtIDq4e(rTFL$"\4YI#\/H'rX>Ś% ^mHq|װc&w1Ck V,8~,G9x7U83αhM'{`j[~ :{d8R`ˢ¨{Yo$/W 8+F v!K\nډZRt4RW2 ,'D Nc%AzLοW Q1U*jWh\uqz ƫPy;{)O܏X]dy%6Nc@m+Ӧ7 93CZ2dk\GuZZ \&4GUh.gzt)([ԩ:"3P0|Sae4J"\ j-t5}N8iB~ XN2hTeM2fNC|I[W<$bDm'ŎIG$%:=C0I{~F+|Khp\  Gv{<~xYaBsz{(z.Îֱˮv~ ҄wvN0TQi:AK-j3ͻ<;43v7)pY`uT;ֽEk1UzHWߍ0S1gz/{[dg0rF-Ym$居1cR&dvxTٚYGn3 ~In]M"~i`Q*3L䤂~V"ľJC\`-}Φj2'?,ӪMpp_41x}p@'w P=;,O Trq qI$lʶQS <D`zu,̨>?R 0CUƙY*h-:5~]krͪ 68Ǎ"b5:;j R靨n/Lic4xOU-h$l uD;rJ99Jqc(v-i|(BHAG"PʌV2@kRUהn{}t RCD Z[ {7sfv_Z ESp*.3Zq@4,ؘ21(Vg#p8x+^x>}ﲹ j0jY]uAn tVvP\>a Bk6o)'׵nV@b9Xu/q >b5@)aGZoV3vr75hjQJ>Lh]L/>+/bXg`z:FK`1d[V7vUk"/fR}yz|1pd2}6a?"ǧ?ha&!o?n> G\n>^ne!GCn%ҦmgbzzmҖ2ISŦogB ^%!>% Y9bmy8cO_}L.)r4r@D A#i!e֘ҶG"%,k#' A9BN̏3O/m]~.M!~Ҏ=*HZ|OBɏX'g!;^he]W Șf4=#EG_"׫]iI`hWs aA* :`u*׿N?Fy+Q:zgxN_w H"}kOe?2eG90 PO8@OodkpM쳣6yCe^z~tqV! Ț`S֌.u1F_+6O[}xD@@J,.uBhr$lx-J\P.;Y }i#kuKJȰ7WƂ$8d"H=U)$HF}2>ŞVG@f@ۦ%QX/( t_c Aq uzy(Č؜8oWvVҝ:0EV>SF!u~R\P;>P~m[TjJSH7ToQ]D2C>'RRLA(V43A6m<0ʔ2}qyToPei6xl2dLRJ*N#Bt8VOET OQb MQ 2|]S_I_WB[P (q!mTdP*-LBu9{xSOOoAYZӅ-PKY4e]1Rg,ϑ -SJb(Rڋ̫iIoP<7F3%9DQDqeV>^/9^3Mj\1,4ʏPWM"=8Gj1Nu*smenEMq^aC-}HA}P>efnv\= T+ȕtby29#pW#$>JjL[I%,5/ H YM  fW01ex!m0fɻ^ƘgE& UʥD0+?vK<~%2Rry9T7>.Q⌎ xf6Ҧ1hF?#3=CF'w@?4oD'sgI" )>mk!5 'vߏ0Vc+ a`p>ˊe#.§,+4X=G(1{1߭ݓhѧz)r~;-g>v/?(:dYdqA26vIHPZR*nIA5TVs3"kX85T-;Ttq;pS;NdkOVIT,B7(V ^f_L jxMRK8A(\ؠDa>[Gմ^Rk5n ^r0A`XBknz-2 R"69k0j\49K2%5r#ruTZ|Uv{r| UDcONC"S4yHu&)௜EReMh0wfS11&o_;g='흚+I5Yf@I_[Ŕ%a]δVXY;mۨDIJ 3f:3>Q@&s9@xrJJK\P"Ki>ǚPszlŷZ]H/=REg>|__\*djɍc7wK:֞E@L-Nк?9Gy7R)ޜ(r"n09v )n uGH#[#E 7pp F:yo*h,˭g(] g7 ~7&zєr(aw%՝u 4TVtU"C̦.M) MV9+ޔMo[eraǐ e>g=夞dV<%у"C/ SQZjNsQ+'LOA05=7%a:nBRĕzp6sGgB /1R{QR%V .ġw*!f8RI|td߱@aa`0vتB8yO>[󈦠~ġDq͛;j<%iZRgq ;#ހ YI]HQgģ'"GqG4>+&3(G(Äf XKIt SAi{\;ǎm(}#zRfitZ|HM)щ rE)Aϔ3 1H|z8QoL2B䃘k &?}kc(^S.Tg=8peb Ћ^Ȍ0xNf&?Ok|'^W! -xC>}˻_>5GxJPG{2 ;u0yQe[r/Jb-*Y5GgY{{P6N!ٱcK'lT',p& OdnjKKș5eŻyajec]5S6%X5%27ɩYN1N I1KV2u>H=nŀSBzf7F.~A+955}_ r=a/"P 2D i 3{Kv6*ȚNcnCb4HeCʬ蕖oH&dH3hHA3CEzAeyBs_m8Do/Tf䣴QefO$[ne(rqzu?hzQ)idj@M_A)D{iWbm]0&fAz|* 18:U踙4[u99ڷ7m][ϕUԵDN1LKn0q1~hp123%~h'3@nbzMLp錛e? oswXg߱Zi|`Gm3a]W~B49JpJ9 άCx=A c/0kzn*YQvx\J+b }W,u[bڂƮ;M+QvZ!L&,KN5ϳId)'eΑYwVAvQd[6&Y&Z V&-iVbV$5`֠VsFYʛj8,5=Q+\Avܦ!<3:n%Q/C 4Si^g +<ϲ$R8v6CNؕYdi؍KaYC7NχFQFyZGx/f%yCdB0|1‹GdL̦^*oL uk N!eLmrdž3Y%m[^ݨSFKMrͺGSfpSmwIF9Nd7 l QO@r%ݷb;@4WOMvZ zOєw:]Ufnؾ@ޔ{k/qsappstream-generator-0.10.1/tests/samples/debian/dists/chromodoris/main/binary-amd64/000077500000000000000000000000001506754475600304235ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/debian/dists/chromodoris/main/binary-amd64/Packages.gz000066400000000000000000000122461506754475600325100ustar00rootroot00000000000000nVPackagesZێF} +43wAcX ۰ejxIk5,K_1{%{"IVݲbdwU1qD$_J}!7QYն]xX5BzYUքg[,,}v-wWVVa_nm& Z+:U'gmf5gom`?{] Z{îZ6]*ٻݮ^t7uН7mf욶԰ߛmXvF. .7o0U^bZ -4 -g 3i@eU$$p2qc%FTƂ/O+'/2@@K?/7 @ue}T4,ШaB ʶg`yX*@U^pmN||eKUYkl*f([on<<)hΰ`$V%xxCXHU-@$IS=l2f]8*1ʦL02kb[Hp42 ,NF2ܙ\G<"Uj$˕i!tµyHQ)"Wa^ȵÿ%w!(B{W w⃩˛Щr #'a?`9l:x^N7jW+ٝvlO /$$i7AUv sV|C@)aʶ+?|C*W4('U=6 tV15w4+ v3lwd^xc~5a7E)5d5áojހZ]5C#oU#+l;ٰP lGѶ-$(k]  @Y7=o[Ry ?aS:qɠ7OΞC:B$5BmKlBη7okf?K?ξ=?Me>; @]y)#4@Dk}BW@3ECW6TsfeIh\<Ϳ=16ԯ]d?CgD9J!>gOW |PeHoM}87%93#-;Ph|+Cϱڣ-b44.\&^QsYn'ԅ6 8k)fr,c)Jc@;P '5 jZPUGQhPu7Y ^Pi_ayݘ£)vmRLC%9iZyP7'v{8/y;oޏ@1\yZ߉gZRu2ˢ߯U'Kg3Y`dxL_Ƽc<؁ njDy[ueQ‚rDa0ԥ+ kJzX%k|sG|jl˴$PHaL dR7WA;SFmw7;LLhwmAiju&%@%ˁw.@$fg)>\!޽͍V!mC8%|;Tl}l%5rd;}gA< /ĖFX=Dd nZm[KEM |֔}X|zVBkгHn7c~@D4h'>k۴K铙APnƃnebCwͩd#D'Ɉ>=: <9ZB7G;NRvس%xUyQ>["~E8J&p'O}z 'xifɸXu,υq6O9NFuBdFg87RgP -H Al/mAqΤFsĖ)NKkL$cgҡ{~}c|"e; l^0<:dHA%1yes\Ȫ\ίY: їdQ32cLƀЂ bSc י*vq "Lf\0P' 8 5x-y*o^80ʔ.2iKˣYQwQmvжmI}l-߿=%>,E8 s>\$ք;ͻvzE̹CÉLZٕ~Ua^Lz>ƼVoݦ@ʺa$Y[)K fd&U.֦]-"_(gwj6ޕ>3E]S>8[w\h, ;8 qɠj?_Ox;oJKY,"Y* eApTi "#9Q$P̹cG0v:Ӈn0c +GZ8$BU|5/<a?K{&1fQ3#٬<FI*KgbGej`.mٯ qۍnhD1ʫ}hV2M+4X/~lJ}~|,}v+ahw`jX|3hwl=b\p'%Z(-j*`aJ6(7ApMAG(B谔4 ;D9| ]0(JV!F HSd¯oR6~s2| SďT+NIhrh*LZ~ʼnr)]Z@51[C M;c K6*)ꆸ H[`Z赁F!/7appstream-generator-0.10.1/tests/samples/debian/dists/sid/000077500000000000000000000000001506754475600235315ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/debian/dists/sid/InRelease000066400000000000000000006306541506754475600253410ustar00rootroot00000000000000-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 Origin: Debian Label: Debian Suite: unstable Codename: sid Changelogs: http://metadata.ftp-master.debian.org/changelogs/@CHANGEPATH@_changelog Date: Thu, 28 Jul 2016 09:29:12 UTC Valid-Until: Thu, 04 Aug 2016 09:29:12 UTC Acquire-By-Hash: yes Architectures: amd64 arm64 armel armhf hurd-i386 i386 kfreebsd-amd64 kfreebsd-i386 mips mips64el mipsel powerpc ppc64el s390x Components: main contrib non-free Description: Debian x.y Unstable - Not Released MD5Sum: 76936f00190cab4ae8001838366675c6 1318264 contrib/Contents-amd64 bfef3ff1a24f2458f4aa0daa8db8809a 23860 contrib/Contents-amd64.diff/Index 9bbc070747a7fbb835e1bbd4b28c1efb 101470 contrib/Contents-amd64.gz 45789bdfeebc45e9d3f2393b0457080e 1175578 contrib/Contents-arm64 59ef2e526db59981b2f697376317c89c 18448 contrib/Contents-arm64.diff/Index b585710cb7b529f99babeb018a751995 87797 contrib/Contents-arm64.gz 9616ad3465885d02f89ce99b09c18bad 1177832 contrib/Contents-armel f42265fa148e7beb1f36d8a95185109d 19432 contrib/Contents-armel.diff/Index a74899c8f4800f519ba25c6b5ec9f1a5 88158 contrib/Contents-armel.gz 6e95aa32591033b7a46851f5762215f1 1164602 contrib/Contents-armhf 68f315bf0c2fb1b8f0ea53c92b142d48 19432 contrib/Contents-armhf.diff/Index eb0f7db757ce657c2edc96b9ee8a9d34 87806 contrib/Contents-armhf.gz 38cab633587a72acf54abd8411ef10c0 1103107 contrib/Contents-hurd-i386 9f78d5d835696aca78d83f7516e7bd9f 18940 contrib/Contents-hurd-i386.diff/Index f1362e02cb534fcc7aabc4bce3cb7b0f 80883 contrib/Contents-hurd-i386.gz 12a23604a09cab75c561dc3038349b27 1290908 contrib/Contents-i386 ae9a0202d7b4ef970e9d91156186430b 23368 contrib/Contents-i386.diff/Index 0af8716ba23577cf2bbe7037d8085c28 99126 contrib/Contents-i386.gz 61898d041d0bcb5ea76278ae8ea9b144 1110069 contrib/Contents-kfreebsd-amd64 4907d62cb00264448fd093b647935563 17956 contrib/Contents-kfreebsd-amd64.diff/Index 8aadeee8fa90ac0a634181983f15484f 81795 contrib/Contents-kfreebsd-amd64.gz cac4d7a16d185e4605c8b3af2956ed1d 1109182 contrib/Contents-kfreebsd-i386 e757b31a95be3c13da48be30b24fc861 17464 contrib/Contents-kfreebsd-i386.diff/Index fc3f02dd8748ba208c0178efaa75b1b8 81545 contrib/Contents-kfreebsd-i386.gz a1a09e6b574814d24738516662ebfd28 1159398 contrib/Contents-mips 0936793b8f08ebc300238341e49654c8 19924 contrib/Contents-mips.diff/Index 4b180f2c69ef52d31f541234c7219b2f 86916 contrib/Contents-mips.gz 4d320fb7d8d6c187cc96350df207c435 1151939 contrib/Contents-mips64el 5bc2d41d3ce5ee999616f51f7b5d6346 18940 contrib/Contents-mips64el.diff/Index 8e243ddd024a70102ef0824877d06c93 86243 contrib/Contents-mips64el.gz 99d29dc09a61774dad60f7d69ae98eff 1159421 contrib/Contents-mipsel 8b404ccb477256c402724384a0efd039 19924 contrib/Contents-mipsel.diff/Index a91548dd6caf580f18232f971ffbf021 86916 contrib/Contents-mipsel.gz 4dccbf22e95bb24f37b2467678c11ffb 1180561 contrib/Contents-powerpc ca38e407233736ff497ae81f5409ef5b 18940 contrib/Contents-powerpc.diff/Index 6fb207e6264260bfaad79316a683fe76 88488 contrib/Contents-powerpc.gz f88f3a5246a95ab161511ccdbbffb514 1142562 contrib/Contents-ppc64el b3b4f516282cd659e37a01dd45eb3ec2 18940 contrib/Contents-ppc64el.diff/Index b1114d1d622a1394424d5c80a8f31948 85835 contrib/Contents-ppc64el.gz ac54b385917a98856179ed8dfb914dbc 1155432 contrib/Contents-s390x ad6daa027695744e5429b26045b4af80 19432 contrib/Contents-s390x.diff/Index 71d6f26fcd2323d63c0a84e39eb41dbb 86413 contrib/Contents-s390x.gz 5f21f15aaca421a1beb2727ca531bacd 3519818 contrib/Contents-source 18b82875a5dc6fd42142dcafaf44e798 24352 contrib/Contents-source.diff/Index 95bd86b0649fe75730b7a7c90e09c330 389700 contrib/Contents-source.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-amd64 b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-amd64.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-arm64 b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-arm64.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-armel b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-armel.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-armhf b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-armhf.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-hurd-i386 b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-hurd-i386.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-i386 b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-i386.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-kfreebsd-amd64 b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-kfreebsd-amd64.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-kfreebsd-i386 b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-kfreebsd-i386.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-mips b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-mips.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-mips64el b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-mips64el.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-mipsel b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-mipsel.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-powerpc b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-powerpc.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-ppc64el b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-ppc64el.gz 68292125276158d468c91a58be0f30b1 451 contrib/Contents-udeb-s390x b49002146fad4eb7e4dce23bc3f48e38 271 contrib/Contents-udeb-s390x.gz db07a500b8e2d51a1c6165f7c7d49263 90903 contrib/binary-all/Packages 035ea549e0c204722715991e2c48fd3f 26745 contrib/binary-all/Packages.gz b47817473f85f3dc1166283d6dbb9970 23788 contrib/binary-all/Packages.xz e00258bbbf38227aadbea5bf9f08af11 105 contrib/binary-all/Release cc6ec5981c3a16e2b5f2d5d1c10b83ca 224553 contrib/binary-amd64/Packages cf47005475a2d07a2df1f7e2efad5424 27796 contrib/binary-amd64/Packages.diff/Index 7464e2b4bc1ddd4e6ba5ca25c3bfc987 62636 contrib/binary-amd64/Packages.gz 079d8fff6e0703ed548c1ca2bc772641 52468 contrib/binary-amd64/Packages.xz 465133a3a391b0f6285452fbcc621120 107 contrib/binary-amd64/Release ccee7273456185be5c0f53645145cb5a 173735 contrib/binary-arm64/Packages 69a6ad82fab38b1703abcf309aa7b1b5 27796 contrib/binary-arm64/Packages.diff/Index b4320a9705f7b81ed37cf39813d66091 48846 contrib/binary-arm64/Packages.gz 6e5e7f45a525e9a727c48e7aa049340a 41604 contrib/binary-arm64/Packages.xz 70e72d09cd1216faff1786b186f7b629 107 contrib/binary-arm64/Release e0c8be877cd53deb2ee92c8aff46f475 175162 contrib/binary-armel/Packages 13dda2b297a266f2c19b98abd92dff1d 27796 contrib/binary-armel/Packages.diff/Index 769ebb9f9e23f8cb5238d2b1eb895d17 49399 contrib/binary-armel/Packages.gz 472f6b9e57038c3f4f30f93406c6945b 42060 contrib/binary-armel/Packages.xz d7da68ee268629ac14567afdb9c16075 107 contrib/binary-armel/Release ad63a40a45ff953ec3f62620366b906e 181954 contrib/binary-armhf/Packages ba028858b08e6027e0fabd2e60f90684 27796 contrib/binary-armhf/Packages.diff/Index d0092f2594449140e4f010b088949f16 51101 contrib/binary-armhf/Packages.gz cb8631172db58f6e7f938c3ecf28008b 43400 contrib/binary-armhf/Packages.xz a2cb49824f6ea7e5fc6c3398d93f6c90 107 contrib/binary-armhf/Release 181d8cd4fe1abf6a0348d9cbd600efd2 147251 contrib/binary-hurd-i386/Packages fac098136e0c11301cfe90c102e052e1 27796 contrib/binary-hurd-i386/Packages.diff/Index 1a1e4a66c5ae37926fdd73af40cccac5 42491 contrib/binary-hurd-i386/Packages.gz bd6bb3766b9fbd05b8b9c043ece636a0 36440 contrib/binary-hurd-i386/Packages.xz 5de9e1d5c34c6429e3280c611901acad 111 contrib/binary-hurd-i386/Release 2ebdb99ac445fd1e9772ed59b0c3a4cb 214181 contrib/binary-i386/Packages ce79e3f92870afca25f1df71e4cb8c43 27796 contrib/binary-i386/Packages.diff/Index 5bab9a27ffa70a483e211281259f8b55 59731 contrib/binary-i386/Packages.gz 312215b0ba43ed704660cf713f76181c 50404 contrib/binary-i386/Packages.xz 78512545310e20e1a5269eeec8964b85 106 contrib/binary-i386/Release 1e54f91f258b788190f48119b88c40e2 156547 contrib/binary-kfreebsd-amd64/Packages 5058edabd42c8d7208ddd7f177a06b9b 27796 contrib/binary-kfreebsd-amd64/Packages.diff/Index 23e787dd54c05786365f596682a08405 44496 contrib/binary-kfreebsd-amd64/Packages.gz ea98bda487f9dab96c3807d925bfb143 38132 contrib/binary-kfreebsd-amd64/Packages.xz e50c34bd5f47b30a4bcb86bef7975614 116 contrib/binary-kfreebsd-amd64/Release 9778c40897b37b57ba2924f3e99e8b8a 156427 contrib/binary-kfreebsd-i386/Packages b910c20a6f70cd62fc36b362f30b03c4 27796 contrib/binary-kfreebsd-i386/Packages.diff/Index 8e0cb14dc33a1b37009eb0ff7c9cd5d1 44488 contrib/binary-kfreebsd-i386/Packages.gz 17f02a26c38946428018c8c672c7bbc0 38184 contrib/binary-kfreebsd-i386/Packages.xz 1ca18bf7c94cd46cdaf297567a59bc9e 115 contrib/binary-kfreebsd-i386/Release 0d78956fc1c55b256dfa21c1770f2a9e 173496 contrib/binary-mips/Packages eecc813f58f3b8980415e1483c3a8b87 27796 contrib/binary-mips/Packages.diff/Index a59e89c627cf65d148f2b865c3d31916 48888 contrib/binary-mips/Packages.gz 9b9a41523ca7d7fe2711df2526b43c40 41788 contrib/binary-mips/Packages.xz 96afd38f08284a16cb242eee20402fd1 106 contrib/binary-mips/Release c2023c4785237ff0569511b1be28ac29 167584 contrib/binary-mips64el/Packages c998e06384ea1f1d7c5dc0b9dbb23b94 27796 contrib/binary-mips64el/Packages.diff/Index 1d623dfe80c49e48557457d1978c5aa0 47081 contrib/binary-mips64el/Packages.gz d02dc52e7e1c93be4368546a501dac56 40424 contrib/binary-mips64el/Packages.xz 3b13c342bd7a0607c43a230cc7138790 110 contrib/binary-mips64el/Release 996788368459cd1955bfab1aaacb1f87 173896 contrib/binary-mipsel/Packages 6c763cd323977a5957a4ef90c2ff95de 27796 contrib/binary-mipsel/Packages.diff/Index e7c7f731589e0e4a0a8a5dd612bf80de 49030 contrib/binary-mipsel/Packages.gz ee073d5641887bf4a17420f1674dcee0 41792 contrib/binary-mipsel/Packages.xz 74cf4bae45fc7682a08d6d8f1e6dd760 108 contrib/binary-mipsel/Release 0407fc65942986fc957b1633a28e3768 177845 contrib/binary-powerpc/Packages d2de0ec9f243ed2155323f2e73e7ed62 27796 contrib/binary-powerpc/Packages.diff/Index b5423c69151e767d765fc3e43bf77581 50212 contrib/binary-powerpc/Packages.gz 7d06e3a3ac87ae984bae3a31253e80a3 42644 contrib/binary-powerpc/Packages.xz 14c6768d1b56622a250e90692b59a46d 109 contrib/binary-powerpc/Release 6e8256f750f2c308408e3a0f841de602 172740 contrib/binary-ppc64el/Packages 42e8c4480c8bcb0fddda6e5968b20048 27796 contrib/binary-ppc64el/Packages.diff/Index 39700d0783456519411c94038ab716a6 48549 contrib/binary-ppc64el/Packages.gz 3ee7fd2fb2bfcf64d2d5d90fc14bf9f1 41448 contrib/binary-ppc64el/Packages.xz b90b505d19146c0e066863ea36412c80 109 contrib/binary-ppc64el/Release 0598d44f3e15f67e367aef8789931f9b 170484 contrib/binary-s390x/Packages 2040deb0094389f1179e1477e30727c6 27796 contrib/binary-s390x/Packages.diff/Index 9fda9aad132aacc8049a29d4a56d2153 48250 contrib/binary-s390x/Packages.gz bbed3a3f3ca3e5e7fe1b4a7692bd2a50 41044 contrib/binary-s390x/Packages.xz 3755c075b383fc15a93ab8b29ce641fe 107 contrib/binary-s390x/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-all/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-all/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-all/Packages.xz e00258bbbf38227aadbea5bf9f08af11 105 contrib/debian-installer/binary-all/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-amd64/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-amd64/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-amd64/Packages.xz 465133a3a391b0f6285452fbcc621120 107 contrib/debian-installer/binary-amd64/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-arm64/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-arm64/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-arm64/Packages.xz 70e72d09cd1216faff1786b186f7b629 107 contrib/debian-installer/binary-arm64/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-armel/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-armel/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-armel/Packages.xz d7da68ee268629ac14567afdb9c16075 107 contrib/debian-installer/binary-armel/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-armhf/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-armhf/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-armhf/Packages.xz a2cb49824f6ea7e5fc6c3398d93f6c90 107 contrib/debian-installer/binary-armhf/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-hurd-i386/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-hurd-i386/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-hurd-i386/Packages.xz 5de9e1d5c34c6429e3280c611901acad 111 contrib/debian-installer/binary-hurd-i386/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-i386/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-i386/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-i386/Packages.xz 78512545310e20e1a5269eeec8964b85 106 contrib/debian-installer/binary-i386/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-kfreebsd-amd64/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-kfreebsd-amd64/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-kfreebsd-amd64/Packages.xz e50c34bd5f47b30a4bcb86bef7975614 116 contrib/debian-installer/binary-kfreebsd-amd64/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-kfreebsd-i386/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-kfreebsd-i386/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-kfreebsd-i386/Packages.xz 1ca18bf7c94cd46cdaf297567a59bc9e 115 contrib/debian-installer/binary-kfreebsd-i386/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-mips/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-mips/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-mips/Packages.xz 96afd38f08284a16cb242eee20402fd1 106 contrib/debian-installer/binary-mips/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-mips64el/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-mips64el/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-mips64el/Packages.xz 3b13c342bd7a0607c43a230cc7138790 110 contrib/debian-installer/binary-mips64el/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-mipsel/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-mipsel/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-mipsel/Packages.xz 74cf4bae45fc7682a08d6d8f1e6dd760 108 contrib/debian-installer/binary-mipsel/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-powerpc/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-powerpc/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-powerpc/Packages.xz 14c6768d1b56622a250e90692b59a46d 109 contrib/debian-installer/binary-powerpc/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-ppc64el/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-ppc64el/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-ppc64el/Packages.xz b90b505d19146c0e066863ea36412c80 109 contrib/debian-installer/binary-ppc64el/Release d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-s390x/Packages 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-s390x/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-s390x/Packages.xz 3755c075b383fc15a93ab8b29ce641fe 107 contrib/debian-installer/binary-s390x/Release ce92e6dfe91c9390868e0f3e4cf6df8f 37465 contrib/dep11/Components-amd64.yml d47781428f68e71f3320856df3252808 9645 contrib/dep11/Components-amd64.yml.gz 114dbfdc20818ddbb3b17f60ed129161 9052 contrib/dep11/Components-amd64.yml.xz 5fffee3d51fc8b194b027c25c191d163 33085 contrib/dep11/Components-arm64.yml 7156b72fec41d12624350bca57e5fc57 8364 contrib/dep11/Components-arm64.yml.gz 4651beb52d002f94d1871cd2d3614368 7872 contrib/dep11/Components-arm64.yml.xz 0afdc6e63cc65122c28f499028c4a4a9 33085 contrib/dep11/Components-armel.yml 7990e09e3e99d3082030ee26e430ccef 8255 contrib/dep11/Components-armel.yml.gz f833ee70cac40f2a65e99ed857963680 7832 contrib/dep11/Components-armel.yml.xz 464c4a3e1ba350097d8680df4a734435 33085 contrib/dep11/Components-armhf.yml d9211402d4c2964a4fbcc9b2fe652b8c 8344 contrib/dep11/Components-armhf.yml.gz 1db57318262e97a48dba53e17c9c0f8c 7860 contrib/dep11/Components-armhf.yml.xz e3ff4f7307186538b127f37345b07447 37465 contrib/dep11/Components-i386.yml 4a7c9a72f94c91cf233a6c91767e532f 9620 contrib/dep11/Components-i386.yml.gz f50339d2be9d743aac18cf0a05a5e11b 9052 contrib/dep11/Components-i386.yml.xz ad46de43c30f96bb42cd5533e43d173c 31558 contrib/dep11/Components-kfreebsd-amd64.yml d8d82ed4b793161ec52db891285a9a3d 7777 contrib/dep11/Components-kfreebsd-amd64.yml.gz 75dee44d1dcf47a17f1829bc3c795c55 7328 contrib/dep11/Components-kfreebsd-amd64.yml.xz eabf301f181ae921726c9920248d1e7d 33085 contrib/dep11/Components-powerpc.yml 16657094afc59692c45be4125329ec61 8314 contrib/dep11/Components-powerpc.yml.gz aeb166ef90550957bec79ca35c253688 7864 contrib/dep11/Components-powerpc.yml.xz 9bb126b19f2888542a1ae114b7c2a4af 372224 contrib/dep11/icons-128x128.tar 94860c8d9551a0b04c101126043bf69b 304821 contrib/dep11/icons-128x128.tar.gz aa64c0a5409688d71fa34973339bfe3d 160768 contrib/dep11/icons-64x64.tar 20fc54d701f75bdac50deeb8801ad200 120717 contrib/dep11/icons-64x64.tar.gz e0f526a83297781edfcdc5204f1bf866 180882 contrib/i18n/Translation-en 71128f827188b4bb28cbd9847f854650 47717 contrib/i18n/Translation-en.bz2 30db714c4408541408d20dcbdeaccd3f 12544 contrib/i18n/Translation-en.diff/Index 96a1ec18a257b9c5da342e87336af24d 108 contrib/source/Release 360cfdcde53f0ffd9fbef781fc813e71 189731 contrib/source/Sources e31b4abf0a8727194f8f8b485fcb0e9d 27796 contrib/source/Sources.diff/Index 5680a4b1ace341717f497b26d49c0dc2 56034 contrib/source/Sources.gz 18441936c32701b1f4c82fd4fdc8b683 47676 contrib/source/Sources.xz ee316089d76ec1b3157e7e602205706a 486486404 main/Contents-amd64 8736f7d7d3c00865bb699339da3378ac 28024 main/Contents-amd64.diff/Index 1a77b48abd1d53423d2b7d0ae206cde6 33741709 main/Contents-amd64.gz 38db2b28d8fa75e08e4be36d9f34a790 464406406 main/Contents-arm64 d65b379a3c964c199434eb746f3d4ac7 28024 main/Contents-arm64.diff/Index e4ca2a7f4ce99ee7e0ff0c7fe6120c04 32427484 main/Contents-arm64.gz 4a6e2f3be652de4b53fee4ae8fc65cf0 462714353 main/Contents-armel 6d9a1550e947646f09155a9a742cc87f 28024 main/Contents-armel.diff/Index 733848b5a7d4bc790ac4049524a7e93f 32372160 main/Contents-armel.gz 55fbd13d082def8c31a7ebc389e86325 465331124 main/Contents-armhf 774925c7655d72dee4a8cfd62e64b9be 28024 main/Contents-armhf.diff/Index ae9463dba41ea69ccb5367b7eed8d07e 32551542 main/Contents-armhf.gz 6aa25806e19961331001b5ff64b9a014 437011535 main/Contents-hurd-i386 a5be6b90e1eef3659752df9978815a32 28024 main/Contents-hurd-i386.diff/Index 857f0045f8eed012e88c337911fea1c0 30282285 main/Contents-hurd-i386.gz 30364de05bf12a8449cced18c78f2bf7 484515088 main/Contents-i386 3c20ff9867dc54ff8311ae90d2767974 28024 main/Contents-i386.diff/Index 241077710e7229a4f9c43ad795723bfc 33649417 main/Contents-i386.gz 3631d88d325e24fba0a1e9a66e98d3dd 449136892 main/Contents-kfreebsd-amd64 b099938d9f72059efb46676206d19be1 28024 main/Contents-kfreebsd-amd64.diff/Index 607925cb1665ad6e0b98773683d54a36 31341686 main/Contents-kfreebsd-amd64.gz 999ec3b31309d9483b551be3954d6a62 448627460 main/Contents-kfreebsd-i386 3a78f1e5220f291c6817431f9bbeb07c 28024 main/Contents-kfreebsd-i386.diff/Index ca08c72ab78447398cc48592cd8676e8 31305004 main/Contents-kfreebsd-i386.gz 30e37a196f4cee83ed4383411260bfac 460036580 main/Contents-mips 0707bb5adde70601f195ab49bea34deb 28024 main/Contents-mips.diff/Index 935c7cdc3d205ecb98c83b5597f5dca2 32236221 main/Contents-mips.gz 6f55fd9c61da45fd1e8cfbaa299bbf37 459438445 main/Contents-mips64el 1a87ca3ab23d02394c31d57418241818 28024 main/Contents-mips64el.diff/Index 7c6e9beae58cb39db8333a380fd4dd91 32030589 main/Contents-mips64el.gz 44b1854e38e2ed5f1d4b5eea129e4236 465041084 main/Contents-mipsel 93900fb47da7b47c5331a7d9b245bfa2 28024 main/Contents-mipsel.diff/Index e8f93fa8b1d6aaf7b8517c3fb0bdd704 32541643 main/Contents-mipsel.gz 9353556da7ca10327824835fb4e1e5ed 465654055 main/Contents-powerpc de1614badf54805be0c72cf33d0a027d 28024 main/Contents-powerpc.diff/Index e38a7263ce5cb08e96b2bb948fe26f2f 32538984 main/Contents-powerpc.gz e7a2f63c83949133414374616601c1b8 462882892 main/Contents-ppc64el aa12e85431c9315c4468fd333b2b0a1d 28024 main/Contents-ppc64el.diff/Index 50da71c6c8a0ed04f94ea744a3fdd2ae 32352649 main/Contents-ppc64el.gz 09078c37a3aa277346d09df4ded07d2f 459302164 main/Contents-s390x 2d7987fd743413a9e0d42762b186faee 28024 main/Contents-s390x.diff/Index 5d007fb854cad448b0dc980ec96e72b8 32174741 main/Contents-s390x.gz 3f9f3f180230bcb30a731f2fc06b686e 482453699 main/Contents-source 766aeb64fbed4ff551403313689f2b57 28024 main/Contents-source.diff/Index a75c9bc97dfa4fe9434ee525d24251d3 53745770 main/Contents-source.gz 928e37ebff0cfa9ce1618985c5f37dc7 484751 main/Contents-udeb-amd64 a5a9820e853661e0fe8b9aac0cab5958 38535 main/Contents-udeb-amd64.gz 04c87ab2627b88fc32088f66809ad659 419372 main/Contents-udeb-arm64 a6525b449bde4dfcbad07f351eb5f9b3 34115 main/Contents-udeb-arm64.gz a911b6fba961d936169e7ed99a184c59 414623 main/Contents-udeb-armel feb351d9b874d290d516fe4efbb2c539 32491 main/Contents-udeb-armel.gz 757dc43eb85187560e2cbd417fff1786 504043 main/Contents-udeb-armhf d1286fae424148204ce9c8bbb2069120 39965 main/Contents-udeb-armhf.gz 9ab52d365a5f55c411b45f0b6771977b 266208 main/Contents-udeb-hurd-i386 ed815b721e699b73a7a4285820d2f04a 22517 main/Contents-udeb-hurd-i386.gz 953d938f13d70a717cada03b45f2c0d1 710802 main/Contents-udeb-i386 42540645d271e7b3636d0a8f0a76c775 54082 main/Contents-udeb-i386.gz 3219c6cca0fdf4ecef1dbf2767a6acda 289951 main/Contents-udeb-kfreebsd-amd64 76839f660ea6e3850c43b10457e40842 24182 main/Contents-udeb-kfreebsd-amd64.gz c0bc594c1b8d7633eaab1a45b0e7966d 289869 main/Contents-udeb-kfreebsd-i386 f0819488c505e6ff36622bed54c91e1f 24244 main/Contents-udeb-kfreebsd-i386.gz 143a17bbfd91d49c510d64c74917a2e8 577017 main/Contents-udeb-mips 2bb0fd7e0ea134b96b9031c790892353 44213 main/Contents-udeb-mips.gz 9cd61e3907666da87ebde517eb37869e 723583 main/Contents-udeb-mips64el f55c7401a1680861fb8b55a189dd76a9 53323 main/Contents-udeb-mips64el.gz 31ecad337aa42bf23b8a7c198656b32a 1008989 main/Contents-udeb-mipsel 4c47c966560843331329545a4a1505ee 71028 main/Contents-udeb-mipsel.gz a259b0594e077f1ce85e735a8a31532e 583828 main/Contents-udeb-powerpc c00518e1f8b131c0000fc64934b7bc48 43594 main/Contents-udeb-powerpc.gz 9605e715d0da43c2703d028833b1a7c0 396155 main/Contents-udeb-ppc64el 03c887f7e6af3ebd8f8dae91b477b055 31809 main/Contents-udeb-ppc64el.gz e77d64695018da365373e7a7b44134c8 307502 main/Contents-udeb-s390x 327c1a251237ba0ca23db736f54528f6 26131 main/Contents-udeb-s390x.gz 8ade86bbd39832fc327cb974cea3a596 17781690 main/binary-all/Packages 83b43a550aebbf511d73f1efd74e4296 4391664 main/binary-all/Packages.gz ccd2fe9f6c28333f59a81a16ea5330dd 3337812 main/binary-all/Packages.xz 10085e8cf6c39e33baf83b03965074f2 102 main/binary-all/Release 9d15c1d6773cd2a24e580d556a636c8c 39256459 main/binary-amd64/Packages 380e3c0605f49bd1937e96f6e6171fc4 27910 main/binary-amd64/Packages.diff/Index 3bf28cd4699902485fb817afca2521ff 9576288 main/binary-amd64/Packages.gz e65fb137990e2ee6449910bc122bc1d7 7147528 main/binary-amd64/Packages.xz e3a090a211110a1a0fdffc2cc7a1c915 104 main/binary-amd64/Release 8fe0e406851d474ceaf3369468bd41b0 37571520 main/binary-arm64/Packages 60efb4ecdc4242c296ed5889f36706fa 27910 main/binary-arm64/Packages.diff/Index c908fe0fb189cd2de4308fd22a3cc349 9227318 main/binary-arm64/Packages.gz 019e1c8630c4b2174921061629aa0579 6884520 main/binary-arm64/Packages.xz 806d63700dac5cea4d29e6a86e58597d 104 main/binary-arm64/Release 4b36b97c3b0e47a788af02d0d2c7b4d0 37699046 main/binary-armel/Packages dbf57f1b126980bbbd7aa4634f0f164b 27910 main/binary-armel/Packages.diff/Index 2c60d28349f8c6c687fc37287b3d0a0e 9275300 main/binary-armel/Packages.gz ab9ddab168b634af1462f1bb0f5a353b 6921736 main/binary-armel/Packages.xz 5c1866d5f05996c75c2150ae69ad9815 104 main/binary-armel/Release 24762c704bc847754986b33caad4a738 37788052 main/binary-armhf/Packages 55af3c509d618db8d9f61e527d0afdf4 27910 main/binary-armhf/Packages.diff/Index 51ada163986a84e78bebd0b03b558024 9300334 main/binary-armhf/Packages.gz 1299022e3a2b7f332e6b65d20a05f3a0 6934520 main/binary-armhf/Packages.xz 87de7b798ba50f1e06ba3ee7a1db41b4 104 main/binary-armhf/Release 19fc25a37c5921182d993cdf0780b760 33806130 main/binary-hurd-i386/Packages 556b6b5ec18daf59f55191ab6c3b9ef6 27910 main/binary-hurd-i386/Packages.diff/Index d4590d9af46bd2435fedd57ef85c4335 8234995 main/binary-hurd-i386/Packages.gz dca28a9d3407408ca288e32a5adec10b 6163664 main/binary-hurd-i386/Packages.xz a84d93ba0ec25a5d1986ea4fcd76d92a 108 main/binary-hurd-i386/Release c06fa33ebe82447a723cef2af9d11a8e 39044462 main/binary-i386/Packages 3bc70aa8440f0a1caf914d5ac142123f 27910 main/binary-i386/Packages.diff/Index 81501e7c090fe73023f9351b1e02ce85 9548697 main/binary-i386/Packages.gz 910d74a441a289bab8de245237aca84f 7122292 main/binary-i386/Packages.xz cf30be831f54859034d2e11501806247 103 main/binary-i386/Release 47f030c0fa64704d93015f8b6b144bb8 35913320 main/binary-kfreebsd-amd64/Packages 9659e819847108e0e39f9e5e23d6b1d3 27910 main/binary-kfreebsd-amd64/Packages.diff/Index 5ec898199fe93755d1fb64c86e224935 8718641 main/binary-kfreebsd-amd64/Packages.gz 44f38681311cea43878ab21aa70496b8 6512728 main/binary-kfreebsd-amd64/Packages.xz 05d24fbef387f91e082ab5dc7c46b9ee 113 main/binary-kfreebsd-amd64/Release 0e46ca35edca54c68c6fc6fa40cfffc8 35809649 main/binary-kfreebsd-i386/Packages 1cf37e1a15bf701fb58276911deb792e 27910 main/binary-kfreebsd-i386/Packages.diff/Index 83faa3a7b131530d4c1f135facae2bda 8704036 main/binary-kfreebsd-i386/Packages.gz 6789374d0ef709221e9683719bcf21a7 6502680 main/binary-kfreebsd-i386/Packages.xz 262a86cd91b0d0d5fb921a317d76df0e 112 main/binary-kfreebsd-i386/Release 229a406feded170996ebefb13ec08cd3 37366754 main/binary-mips/Packages 53f100c30462cdb18aa9f097c0394d42 27910 main/binary-mips/Packages.diff/Index 3a348375ed9851ae75692a436641ea10 9210212 main/binary-mips/Packages.gz 1c1bc0b691df9cfd5790d2055f648c68 6873512 main/binary-mips/Packages.xz 9d9f639b9f52548b2eb8392e691a5c5e 103 main/binary-mips/Release 5b3a92396132930feb2c5d1afbccac67 37340330 main/binary-mips64el/Packages bf4b6c4f633568da3717a24c9163ef7b 27910 main/binary-mips64el/Packages.diff/Index 67cd956d14111c950b0b0171aaee3a96 9156063 main/binary-mips64el/Packages.gz fc0ee60864c7483d6a8d98a81e2047ad 6834608 main/binary-mips64el/Packages.xz 8246be42dc917113cba664e98bd50d3a 107 main/binary-mips64el/Release 5e364fe4381b0d18d9fa3407a13607c2 37668023 main/binary-mipsel/Packages 7e63b52935765be1f4f8f41bfff932d1 27910 main/binary-mipsel/Packages.diff/Index 93887e92bcede6b341b1f22762b4630b 9262851 main/binary-mipsel/Packages.gz ef3c44197a1b9a1a7ff49d5dbbe52bc7 6912400 main/binary-mipsel/Packages.xz 3dc8b9d46dd15d877218c7c13d0e2884 105 main/binary-mipsel/Release 91ad2fa7d560ad9a114562a6f66d9a00 37823973 main/binary-powerpc/Packages 97fcc2b38b44cd0f1a6f1055adc8b8b6 27910 main/binary-powerpc/Packages.diff/Index 8aa5805ab669d80b7174f37beab2061b 9292207 main/binary-powerpc/Packages.gz 99602114bee93a0f56d25dc4f8add0a2 6929276 main/binary-powerpc/Packages.xz 066a5dd8d4763961ca7e72ab118e91d4 106 main/binary-powerpc/Release dbcd9dca16846ab516d809d1fe9c6d3d 37859587 main/binary-ppc64el/Packages 07322d6518dd19ac2e177e1bba7aa39f 27910 main/binary-ppc64el/Packages.diff/Index 1ab9453951f0af4d9c4f19bb6a0b7ccc 9277244 main/binary-ppc64el/Packages.gz 03d1d9b800de33125a75f75e59ecde60 6921148 main/binary-ppc64el/Packages.xz cc8afb9a3d24cea535c031ff83cc1bdf 106 main/binary-ppc64el/Release 6f03bcfea77b6b05e5959c9a48e0736c 37432240 main/binary-s390x/Packages 1d2a46185ed0bb161ae606673816e2b8 27910 main/binary-s390x/Packages.diff/Index 55b2873204d740bb61d1fef7c44f3a9b 9217612 main/binary-s390x/Packages.gz c04e71c9af4f85a3cb3572a47bfb6b02 6875604 main/binary-s390x/Packages.xz 8644bd20faff93a0b7716ca2e987ed96 104 main/binary-s390x/Release 1f772f7b647e73ad65bd8244ccf6606c 63066 main/debian-installer/binary-all/Packages c53b106eab1021074e41b465e7d7cf93 16829 main/debian-installer/binary-all/Packages.gz cde4986c79280dae866d3ac5893cf58b 14836 main/debian-installer/binary-all/Packages.xz 10085e8cf6c39e33baf83b03965074f2 102 main/debian-installer/binary-all/Release 5c134da0018d86ecfa464c17fd4c4807 258901 main/debian-installer/binary-amd64/Packages ff7d6e3aef8ad0d2606f498b9b9b312d 65129 main/debian-installer/binary-amd64/Packages.gz 0a0d7f601780e2e9e251bf77b95835cc 54808 main/debian-installer/binary-amd64/Packages.xz e3a090a211110a1a0fdffc2cc7a1c915 104 main/debian-installer/binary-amd64/Release 7f27f4fd16c5a61495f79920c2b69152 239565 main/debian-installer/binary-arm64/Packages 803a71ee6cb342142e7cab22d068bfaf 61678 main/debian-installer/binary-arm64/Packages.gz a536ce6e1d687a3c14dc6368d9edac6a 52152 main/debian-installer/binary-arm64/Packages.xz 806d63700dac5cea4d29e6a86e58597d 104 main/debian-installer/binary-arm64/Release 0c2f83d7603e1a94a75bb0fb55c2db5e 277695 main/debian-installer/binary-armel/Packages 759db9003daa3771dc6f7df2378654fe 67415 main/debian-installer/binary-armel/Packages.gz 9d496d50885bda800a9b28b082317616 56652 main/debian-installer/binary-armel/Packages.xz 5c1866d5f05996c75c2150ae69ad9815 104 main/debian-installer/binary-armel/Release 216554fdcabdecf7396d03436278a2d8 238490 main/debian-installer/binary-armhf/Packages 412f4ece6f0a6415e671b9ae6f3a5d2e 61449 main/debian-installer/binary-armhf/Packages.gz a65b1e027e43be059b0595a99661577e 51968 main/debian-installer/binary-armhf/Packages.xz 87de7b798ba50f1e06ba3ee7a1db41b4 104 main/debian-installer/binary-armhf/Release 46aebcdbbbfb439976bdf6cbab8cb1c4 163351 main/debian-installer/binary-hurd-i386/Packages d2c999206c127e2756b1fef9bca78460 44433 main/debian-installer/binary-hurd-i386/Packages.gz c53a434fd94adb1f01102945896b018a 37804 main/debian-installer/binary-hurd-i386/Packages.xz a84d93ba0ec25a5d1986ea4fcd76d92a 108 main/debian-installer/binary-hurd-i386/Release fae19170644abeb877ef7cb7fefa8389 326037 main/debian-installer/binary-i386/Packages cb6eb5aa7994ec1603e7b7dfa40eac2d 75140 main/debian-installer/binary-i386/Packages.gz 175c0a257d4384a9920d870c0a887b52 63056 main/debian-installer/binary-i386/Packages.xz cf30be831f54859034d2e11501806247 103 main/debian-installer/binary-i386/Release 31edeb172d84f67e53565e513f72d5ac 199536 main/debian-installer/binary-kfreebsd-amd64/Packages 996c98fcf568bb706df7f12b74d591cb 52052 main/debian-installer/binary-kfreebsd-amd64/Packages.gz b69fe29f33ef0f127075501561e02c04 44020 main/debian-installer/binary-kfreebsd-amd64/Packages.xz 05d24fbef387f91e082ab5dc7c46b9ee 113 main/debian-installer/binary-kfreebsd-amd64/Release d8c67469af6a623d0378a0a77260e952 198799 main/debian-installer/binary-kfreebsd-i386/Packages 12cefc0c7ad5d9485c3f1210e95320ac 51848 main/debian-installer/binary-kfreebsd-i386/Packages.gz ba7940166304e6d4c9226cbf083bcac6 43944 main/debian-installer/binary-kfreebsd-i386/Packages.xz 262a86cd91b0d0d5fb921a317d76df0e 112 main/debian-installer/binary-kfreebsd-i386/Release cd6ba1715c8dd3206a1db6d760aaaf65 299343 main/debian-installer/binary-mips/Packages ecca0c469dc3f398c41cecb551fac41e 70317 main/debian-installer/binary-mips/Packages.gz f97068d8c284bfb47af9b1f762ee9291 59428 main/debian-installer/binary-mips/Packages.xz 9d9f639b9f52548b2eb8392e691a5c5e 103 main/debian-installer/binary-mips/Release cf3dbee9b700902fe4067509874623f8 360842 main/debian-installer/binary-mips64el/Packages cc4a7d0dbb5eac93e698c021eae9f686 78622 main/debian-installer/binary-mips64el/Packages.gz a716d07ad9d6abcf990fd8dfd2c7e344 66228 main/debian-installer/binary-mips64el/Packages.xz 8246be42dc917113cba664e98bd50d3a 107 main/debian-installer/binary-mips64el/Release ace3200c84545df887a44de51ff45a3d 479718 main/debian-installer/binary-mipsel/Packages f2fe95f520fbacc0e344551e1ac6dc60 94984 main/debian-installer/binary-mipsel/Packages.gz 962396873962d1c525397b9d1c1cb238 80008 main/debian-installer/binary-mipsel/Packages.xz 3dc8b9d46dd15d877218c7c13d0e2884 105 main/debian-installer/binary-mipsel/Release dbc98c4149ed3bd0887dc8ebe14877ba 310055 main/debian-installer/binary-powerpc/Packages 57bc261239119875a729b73d9e95b181 72594 main/debian-installer/binary-powerpc/Packages.gz ef0d4217eaab3d6dfee94503a6e7bd35 60748 main/debian-installer/binary-powerpc/Packages.xz 066a5dd8d4763961ca7e72ab118e91d4 106 main/debian-installer/binary-powerpc/Release 31198895a526db6c434912f95566a0fb 241368 main/debian-installer/binary-ppc64el/Packages 0f77f3afb0bcbbf78b4352f870bcdf06 61537 main/debian-installer/binary-ppc64el/Packages.gz 793f00c0010c2ce15f474450f61c7da6 51868 main/debian-installer/binary-ppc64el/Packages.xz cc8afb9a3d24cea535c031ff83cc1bdf 106 main/debian-installer/binary-ppc64el/Release 61441145bd99af408307e3aba37da10e 217996 main/debian-installer/binary-s390x/Packages 911f2e61806f7487700cfcdda189c0ef 58135 main/debian-installer/binary-s390x/Packages.gz 97393e1d43b85a370b055cabfb4c2fad 48892 main/debian-installer/binary-s390x/Packages.xz 8644bd20faff93a0b7716ca2e987ed96 104 main/debian-installer/binary-s390x/Release 1be16ae9bcaa4d71ad54aefa158d0741 7796441 main/dep11/Components-amd64.yml 732f1040515480d9e43b70640103b84b 2854073 main/dep11/Components-amd64.yml.gz c6698a2a0283112674726e53f7fab4ab 1850876 main/dep11/Components-amd64.yml.xz 3d0a7cb0fb243858415b8f7101380071 7735559 main/dep11/Components-arm64.yml a9591dd40c9ca0eea351ddd652da8e53 2835371 main/dep11/Components-arm64.yml.gz 0c8465b31304d95dc287c570f8ddf0ef 1839368 main/dep11/Components-arm64.yml.xz eacbffae318216658b9a18add435a6d2 7744621 main/dep11/Components-armel.yml fdfe5bbb75136248ff776fbd3c7b7934 2837621 main/dep11/Components-armel.yml.gz 160860e7053cb47167c651525b52e39c 1840304 main/dep11/Components-armel.yml.xz 6a8923dce0d3d21ce7a089a69e2e2608 7753412 main/dep11/Components-armhf.yml f0c4edf897f8ca8cc93f7201173a4ca4 2838544 main/dep11/Components-armhf.yml.gz 980acfd0a92bff12bcdd7b83515bc70f 1842132 main/dep11/Components-armhf.yml.xz 37d3152322d44bdf96bdc19da2f8405b 7795011 main/dep11/Components-i386.yml f24043ea4e83a25f76d67e533004a2f1 2853321 main/dep11/Components-i386.yml.gz 54ae9c7371b0593b99e935684b61e4b9 1850760 main/dep11/Components-i386.yml.xz 55ba75bfecfa1fd5cbb1fd4f97dcbe1d 7285280 main/dep11/Components-kfreebsd-amd64.yml 5788f902e8bc2b33ea5800bf4e0b876b 2669347 main/dep11/Components-kfreebsd-amd64.yml.gz 578df9dbc63281664eedea849e60631c 1732772 main/dep11/Components-kfreebsd-amd64.yml.xz 46df1c219af20dd2c1951903df5e7eed 7747672 main/dep11/Components-powerpc.yml 242ab1e62e0bea1e80dbade25f2d2df1 2838839 main/dep11/Components-powerpc.yml.gz 4f980e4a58d23d7f732989dcf796303f 1841208 main/dep11/Components-powerpc.yml.xz e52bdd59f2f7dbbcb176a4f0a43f16b9 17068032 main/dep11/icons-128x128.tar c9efbb8a8005e19429ad2e15a76f04a2 15132129 main/dep11/icons-128x128.tar.gz 650e62ecc56676b2f445b7bbdf2f9b1b 8170496 main/dep11/icons-64x64.tar f643dd47e28d5a08c5f8f688c16596e9 6466066 main/dep11/icons-64x64.tar.gz 0071a785652b9a6f6c7d1dbec292a25c 8628 main/i18n/Translation-ca cab00f4af70523ecdbf645e7f7ee2492 3543 main/i18n/Translation-ca.bz2 c1961e8d860a293b033a6a975ca17c68 1755527 main/i18n/Translation-cs af72078ec1a260d5778aa99c2354e8e9 456958 main/i18n/Translation-cs.bz2 ca359d40eb141112515e2bc9f2032f08 14512 main/i18n/Translation-cs.diff/Index e6aa62edfced0fd7ac54da06eb6deefd 17190796 main/i18n/Translation-da 7a484c530f3ca3dcf5f5b45c0ea270fa 3716728 main/i18n/Translation-da.bz2 fe455625cace78a9bd4494b0d67aac04 27910 main/i18n/Translation-da.diff/Index d3b62a232514714ef632dcafb4df1989 7563488 main/i18n/Translation-de 0cf191dd8ca9aebfefe0034fc720dfd3 1724541 main/i18n/Translation-de.bz2 86e70b7b43dadbfc6cb932293268ecd8 27304 main/i18n/Translation-de.diff/Index a344219bf0eec9139d5270017ecfceee 1347 main/i18n/Translation-de_DE 0fe0725f74bb5249f15f30ce965142d5 830 main/i18n/Translation-de_DE.bz2 c26d1f5d454d59a734435115a463bc5e 8287 main/i18n/Translation-el a2e3d718b49885932660c9c5dbdc0570 1896 main/i18n/Translation-el.bz2 b5e6c14f747fd3d3ea480e4b0fdcb597 27076273 main/i18n/Translation-en 7cd099961072cc7a1a685d18f8b1c84b 5427424 main/i18n/Translation-en.bz2 8248703cda79af6d6df64ebcb607cf90 27910 main/i18n/Translation-en.diff/Index b38a08cc72a01f896fa745a25ef55c00 2517 main/i18n/Translation-eo 82bf9de0303a2ff3fb791f19c84f3c25 1279 main/i18n/Translation-eo.bz2 745a93b46c684078c290249362cdcf30 1326503 main/i18n/Translation-es 691f4e9949f6a13c01b0dc016015bdd3 323379 main/i18n/Translation-es.bz2 bf1ae7fcdadb3150158681852b8117fa 17464 main/i18n/Translation-es.diff/Index fe1a2c966238e27a62bc1d9531978927 14383 main/i18n/Translation-eu c2dc13f9f8a39978e10ae070437a5362 5475 main/i18n/Translation-eu.bz2 a894e9b1a8c56b927d37fc606a2d92fd 736 main/i18n/Translation-eu.diff/Index 48d0c6ee8c241ea6335713ce281e0980 386506 main/i18n/Translation-fi 798db04510751ffa53c3848205b95efd 106618 main/i18n/Translation-fi.bz2 0678f7e2ee77b76f2866b47c88713a03 11068 main/i18n/Translation-fi.diff/Index acf5e6f0027a3756ad32b596aec21793 7401641 main/i18n/Translation-fr 26d9074edbb798bbd1b4837a5343a772 1518856 main/i18n/Translation-fr.bz2 f00d9eeb8f2cc8e344fb76e071fabec1 27796 main/i18n/Translation-fr.diff/Index 54790b326f496d11042e7afa4c4631b1 21899 main/i18n/Translation-hr 9fdbf9e07c96ee5af56030d154ff607f 5692 main/i18n/Translation-hr.bz2 cabf65aab935b431ac8030167ef8eeef 2212 main/i18n/Translation-hr.diff/Index cc423bce94fd7d062ec63881438d6373 96810 main/i18n/Translation-hu e1205e9042cd5be51989f5b4533aa8a6 31851 main/i18n/Translation-hu.bz2 4119e3b251dda916b184607c8514ec78 1720 main/i18n/Translation-hu.diff/Index cb9bfeca0fea2c93ecccdc320b485294 7124 main/i18n/Translation-id d0624bdc42d2602cb4664b190e372468 2899 main/i18n/Translation-id.bz2 2381f089c05c4e0ac6cb5c8ffd600b2d 21789753 main/i18n/Translation-it fdc75537379d05d4bed7acd588b01f1f 4268806 main/i18n/Translation-it.bz2 6cccac8e38b9357ff436204a1a084056 27910 main/i18n/Translation-it.diff/Index 64697acfcd63339ba56af18a4de28a08 5075815 main/i18n/Translation-ja 0bbc55ae9583df8ff28b694b60640a83 913629 main/i18n/Translation-ja.bz2 2045762863e31cddc9166a4e80a84b62 27304 main/i18n/Translation-ja.diff/Index f6fe5be7465cd63813c244b1a1f54c45 19077 main/i18n/Translation-km 17c60ce8f72b68982629b2e469915109 3407 main/i18n/Translation-km.bz2 12087f416d16de7c5ffd6698a9dedb0f 1720 main/i18n/Translation-km.diff/Index f375c313c1e93832540f1e2f47f503bd 1631004 main/i18n/Translation-ko dd61eca2bc9adc13f33cf80f106713d6 348522 main/i18n/Translation-ko.bz2 c8d9248d7a963c2b5aa9bda9b1fa7599 22876 main/i18n/Translation-ko.diff/Index d41d8cd98f00b204e9800998ecf8427e 0 main/i18n/Translation-ml 4059d198768f9f8dc9372dc1c54bc3c3 14 main/i18n/Translation-ml.bz2 5676b1dbf1afed1dfd2ed8c4d87d6f8e 2325 main/i18n/Translation-nb be1e645de35839db0778d87b8e903e43 1302 main/i18n/Translation-nb.bz2 92a2666c8a09bb4cb4fd0649a87adf99 264671 main/i18n/Translation-nl 0b2c46361ecfdeb5025a5cd67f3beb89 71487 main/i18n/Translation-nl.bz2 6c0270d045ed66a981290e1a31177f35 9100 main/i18n/Translation-nl.diff/Index 70503f8efbcb3ab4092ab5198c5d78df 2458040 main/i18n/Translation-pl b6e413762a2dc3b79dc0f7ea6e27802c 569778 main/i18n/Translation-pl.bz2 3d7080de3027e28aa20808e3248a077c 26812 main/i18n/Translation-pl.diff/Index e7a1a2f02d37eaafc8a23437aad0a43a 1564381 main/i18n/Translation-pt 55e4c007261255a9991e72546fd4a3ba 386197 main/i18n/Translation-pt.bz2 2222e20f5c51815f96e3062e0b1d9d25 15988 main/i18n/Translation-pt.diff/Index 3febc6212a82148533ece34ba3012a46 3548094 main/i18n/Translation-pt_BR ed8e05782286ae5b37940b23bb305d72 846754 main/i18n/Translation-pt_BR.bz2 5cfd36393ef4c067d78f7ca3d948a0fa 26812 main/i18n/Translation-pt_BR.diff/Index 60f08a8c65af4e8895cf8c53d1e35b98 2710 main/i18n/Translation-ro a998260e0995f073174d9e0d1fb65a9f 1374 main/i18n/Translation-ro.bz2 b0c2a9c949dcc75045cb71e66a08912c 2655478 main/i18n/Translation-ru ff4a328011b0ff8aad87691a86d220b7 431543 main/i18n/Translation-ru.bz2 9c0448669aac690b8fc06ff2cbff4bad 21400 main/i18n/Translation-ru.diff/Index 88d33ae4bd81a0727d8dce11de7273d7 3819007 main/i18n/Translation-sk 6f2322531d7839e2a27ca48dd65e767a 843783 main/i18n/Translation-sk.bz2 1b4fb1d9ea9d7e58ea738aa8e498a737 27796 main/i18n/Translation-sk.diff/Index 3f7669405e04d1862d6eb8e6817724d5 441964 main/i18n/Translation-sr aa1268bd55a5b92f37682ac36f8cbc78 78350 main/i18n/Translation-sr.bz2 34509802078a71613ebd27a1c42ddc14 8608 main/i18n/Translation-sr.diff/Index 2e7e17ccaed45a46635480f54b65ab39 117165 main/i18n/Translation-sv 20113f96e4db968fa1ad21398e563477 36066 main/i18n/Translation-sv.bz2 1fb32f3ac643d745064a5fdea2152a7a 3196 main/i18n/Translation-sv.diff/Index 617fcb26803e9b2a60c2716ea4744451 902 main/i18n/Translation-tr 96c9deb10e4bb691e778589dec582fd7 530 main/i18n/Translation-tr.bz2 f352a71d97747abb0b7ec10b17df6fed 4277439 main/i18n/Translation-uk 46389306c392dacc4ba9a57a03899c06 669553 main/i18n/Translation-uk.bz2 62b32c0ed3d5609f449841b550306847 22384 main/i18n/Translation-uk.diff/Index 3490920ca94901031bfef0b0179c4530 30443 main/i18n/Translation-vi 4b9f8610634f77136a0ff15daaa2e206 8837 main/i18n/Translation-vi.bz2 31d75bce045815aa393d2275c1e536d7 2212 main/i18n/Translation-vi.diff/Index 7717fa25bd691ec385bddacf42419c9f 2799 main/i18n/Translation-zh b5a4e7e47da47ac7596ed5d72fdeef93 1526 main/i18n/Translation-zh.bz2 cbdf4b385a9e472b4ec44142a2dd6c37 334563 main/i18n/Translation-zh_CN d76c54843dbd3f0e47c236bfc42d8f79 92636 main/i18n/Translation-zh_CN.bz2 145978f6e98c3cb6ff3d37ab5358cb41 7132 main/i18n/Translation-zh_CN.diff/Index 591bd2e425d8755cd1af8b449070ef3e 57122 main/i18n/Translation-zh_TW d565663e6a0417f70b1e1eecd2f0b3eb 20289 main/i18n/Translation-zh_TW.bz2 0bd805f62422f7ee5a4d8cc0f0b59e01 3196 main/i18n/Translation-zh_TW.diff/Index c06312d27a05b395a5a45ae7c2507ab5 53815 main/installer-amd64/20150422/images/MD5SUMS bc3feb487bafcbdfd06e06284000e368 72131 main/installer-amd64/20150422/images/SHA256SUMS 7f129f18d483d8678691e32bb6954436 53815 main/installer-amd64/20150718/images/MD5SUMS b7cab70bdf269ab34b0a5b0d36ec0734 72131 main/installer-amd64/20150718/images/SHA256SUMS 68f30bd7c1f371b82a894837a1643ab9 53815 main/installer-amd64/20150813/images/MD5SUMS 1b9cd6c615f7d0ee59aeee91e089cc7f 72131 main/installer-amd64/20150813/images/SHA256SUMS dcd4a129eb03d7b79764465d97468658 53635 main/installer-amd64/20150828/images/MD5SUMS 7da4e9a6338cbda2de43afefc75ee2f9 71887 main/installer-amd64/20150828/images/SHA256SUMS 515b783412be83349fc94cbcc41f3983 53635 main/installer-amd64/20150911/images/MD5SUMS 30ed9399e20e911c5046120fa65026ca 71887 main/installer-amd64/20150911/images/SHA256SUMS 1f897dfc4d7ae54c3a9d8df23bb7c314 53727 main/installer-amd64/20151023/images/MD5SUMS 6d2f81899356555e441421d51dc741e8 72011 main/installer-amd64/20151023/images/SHA256SUMS c9583f4597e52933689397d09d890d80 53635 main/installer-amd64/20160101/images/MD5SUMS a45f291d4f5c4128e9014271c7b12c7c 71887 main/installer-amd64/20160101/images/SHA256SUMS d6d4aa83312ae55d273bd8385f16aa38 53727 main/installer-amd64/20160106/images/MD5SUMS ab78bc26828b883a79bc9cc23b1286a9 72011 main/installer-amd64/20160106/images/SHA256SUMS bb47b77e4f3d80c9bf892a9d4dd4f8ba 53727 main/installer-amd64/20160516+b1/images/MD5SUMS 421bc2a824adecce9d2376963eb9b63d 72011 main/installer-amd64/20160516+b1/images/SHA256SUMS e4f3a21784d6a3102a130ba20e92f0d8 53727 main/installer-amd64/20160516/images/MD5SUMS 1a3e7aaf715ce42b17cfb6360b16e236 72011 main/installer-amd64/20160516/images/SHA256SUMS b0d1c9a722a2b5affd29b77b902e3a0c 53727 main/installer-amd64/20160630/images/MD5SUMS ce41b2fa73adf559477b37a65d4ead20 72011 main/installer-amd64/20160630/images/SHA256SUMS b0d1c9a722a2b5affd29b77b902e3a0c 53727 main/installer-amd64/current/images/MD5SUMS ce41b2fa73adf559477b37a65d4ead20 72011 main/installer-amd64/current/images/SHA256SUMS c52390d4695c984028c98a32f7a76dff 19148 main/installer-arm64/20150422/images/MD5SUMS f4160f9f0b4b49d8997ee2c920004594 25912 main/installer-arm64/20150422/images/SHA256SUMS 8bb2134b1f6eb311047621b8327b92ce 19221 main/installer-arm64/20150718/images/MD5SUMS 00212197e96943527cd7d877599c1f26 26017 main/installer-arm64/20150718/images/SHA256SUMS 7a76d85513a26f4d247d12f07169a483 19221 main/installer-arm64/20150813/images/MD5SUMS b9582e565c71f9803f145308d626fb58 26017 main/installer-arm64/20150813/images/SHA256SUMS 46aa54cf6adecef384915e9e97139b08 19221 main/installer-arm64/20150828/images/MD5SUMS fc369331103d7c5128e55907b8a65dfc 26017 main/installer-arm64/20150828/images/SHA256SUMS 792e4f72b73996159ba8c760cf005fc5 19221 main/installer-arm64/20150911/images/MD5SUMS 0a85072cf979aa24d6e95e9a2fd05682 26017 main/installer-arm64/20150911/images/SHA256SUMS bbd9976457d3354a20702dc3837a501c 19366 main/installer-arm64/20151023/images/MD5SUMS 3c0d447a3310c59b52634d2536f28dc2 26226 main/installer-arm64/20151023/images/SHA256SUMS 1e7d8258b04971e32b2a1eaaf03f1cb7 19366 main/installer-arm64/20160101/images/MD5SUMS 16a6252eeac87cec2a8a63d5aa643882 26226 main/installer-arm64/20160101/images/SHA256SUMS 81694c96c2c2a89af5094dd5cc62ae43 19366 main/installer-arm64/20160106/images/MD5SUMS 50eb4893fcb29fa5e101ad3d89e16a68 26226 main/installer-arm64/20160106/images/SHA256SUMS a27a3ca9b9201d08a6959c203abd6665 19788 main/installer-arm64/20160516+b1/images/MD5SUMS 24d396d0e93b2f1b994015a7b17b80c0 26840 main/installer-arm64/20160516+b1/images/SHA256SUMS 530f897d2f040bb2171355b978ce3ce5 19788 main/installer-arm64/20160516/images/MD5SUMS b7ff35aeda772a129d9ed692546d0a44 26840 main/installer-arm64/20160516/images/SHA256SUMS 7801f2f68870b7629876cefe1b876644 20520 main/installer-arm64/20160630/images/MD5SUMS 154af14e29eff0e06367dbdb7f319479 27892 main/installer-arm64/20160630/images/SHA256SUMS 7801f2f68870b7629876cefe1b876644 20520 main/installer-arm64/current/images/MD5SUMS 154af14e29eff0e06367dbdb7f319479 27892 main/installer-arm64/current/images/SHA256SUMS 42fdeb26bb11aeba2b73bb665394ed5d 8985 main/installer-armel/20150422/images/MD5SUMS 5508c992d754756a76d94459694d0846 12645 main/installer-armel/20150422/images/SHA256SUMS 68f868a28043100a893c564b0faae3da 10136 main/installer-armel/20150718/images/MD5SUMS af69eb139df5e30ef86e8e9959e82cf6 14244 main/installer-armel/20150718/images/SHA256SUMS 01e076ec44be1c77c251d7d5fc1c9ebc 10214 main/installer-armel/20150813/images/MD5SUMS 652fce61997776934cc902891dfbef34 14354 main/installer-armel/20150813/images/SHA256SUMS 1fe56992ad57188f6c64ef4a587fca08 10214 main/installer-armel/20150828/images/MD5SUMS 2e1860e17b51a02ab3dcd4a6ca2b8270 14354 main/installer-armel/20150828/images/SHA256SUMS ab92cc7d8af85dcbdfeb1c23a1a4ada3 10214 main/installer-armel/20150911/images/MD5SUMS 81239b7aeb188258c05bcd148ba6aca7 14354 main/installer-armel/20150911/images/SHA256SUMS d1ef8153db55b4c7fefea9b5d665268b 10214 main/installer-armel/20151023/images/MD5SUMS 685702eda13193131532e737ac847225 14354 main/installer-armel/20151023/images/SHA256SUMS 907bdf89e1cd0b04c3ea3c5ac5293524 11320 main/installer-armel/20160101/images/MD5SUMS a174d2d7c9f86588e9e6dbd64c1fe95b 15908 main/installer-armel/20160101/images/SHA256SUMS 4d091ea335e1cc28c501fac36f834e73 11320 main/installer-armel/20160106/images/MD5SUMS 303a7bf2d68e0ee414803a694a692871 15908 main/installer-armel/20160106/images/SHA256SUMS b9a3925fa4cc3d6746f3091037410b46 21918 main/installer-armel/20160516+b1/images/MD5SUMS 1cecfbd15243177f6811e81cd44c0fb2 30634 main/installer-armel/20160516+b1/images/SHA256SUMS e2a460d19a1987783987245a64c82843 21918 main/installer-armel/20160516/images/MD5SUMS 985f5387499527325e845ddc5a97207b 30634 main/installer-armel/20160516/images/SHA256SUMS eaf0a967e3ddddbf271a22eb172a253d 22683 main/installer-armel/20160630/images/MD5SUMS db333669e2bc24baf7b5edf891dd2df6 31687 main/installer-armel/20160630/images/SHA256SUMS eaf0a967e3ddddbf271a22eb172a253d 22683 main/installer-armel/current/images/MD5SUMS db333669e2bc24baf7b5edf891dd2df6 31687 main/installer-armel/current/images/SHA256SUMS 77d9da266c887d99f50e4a78770da16a 19599 main/installer-armhf/20150422/images/MD5SUMS e1ea97c8e0f0ab02e0acf66302b64072 28379 main/installer-armhf/20150422/images/SHA256SUMS 084e58bfd24c832bf0518b2087ef57f9 21631 main/installer-armhf/20150718/images/MD5SUMS d331ede290678323e8b2c2ee8b64c1eb 31339 main/installer-armhf/20150718/images/SHA256SUMS fd95ab71c8125b6393c95a658b1c4208 21993 main/installer-armhf/20150813/images/MD5SUMS 53b5644ab2d2dceb5a1aa76a29646520 31861 main/installer-armhf/20150813/images/SHA256SUMS 91a3bb58bdb3f081fd69ee0e5e64d7c5 21993 main/installer-armhf/20150828/images/MD5SUMS d76f6202ffae6064a7e5d8d91b746cdf 31861 main/installer-armhf/20150828/images/SHA256SUMS 6743052cfa5856911fadfec7d185b3a7 21993 main/installer-armhf/20150911/images/MD5SUMS aac8a16428226be115dc66569c3462fa 31861 main/installer-armhf/20150911/images/SHA256SUMS 9af75cfdb021ecfe4ca81d83ade7c198 23951 main/installer-armhf/20151023/images/MD5SUMS a48ab9a8bc1bf849804ffc53eb06e768 34683 main/installer-armhf/20151023/images/SHA256SUMS e25a9a21c87af432d1de665392121307 25780 main/installer-armhf/20160101/images/MD5SUMS 008a0740f85cd184a7da076dc416a0a0 37312 main/installer-armhf/20160101/images/SHA256SUMS ca01103d2b6f761a26d2c1e54d504a80 25780 main/installer-armhf/20160106/images/MD5SUMS 4acd1721546591fcab76797c815826fc 37312 main/installer-armhf/20160106/images/SHA256SUMS 904c6dc5f2981eca165615bf4f5e903e 32094 main/installer-armhf/20160516+b1/images/MD5SUMS 0398c7bcc09de0655cca935474d7b114 46346 main/installer-armhf/20160516+b1/images/SHA256SUMS f435648778a72cc5758610cdd56ac726 32094 main/installer-armhf/20160516/images/MD5SUMS 497beeebd06717cdac51210460acea86 46346 main/installer-armhf/20160516/images/SHA256SUMS b70948b0dd60cec7390d2a38b8124eae 33942 main/installer-armhf/20160630/images/MD5SUMS 69e0668080dd6865f9410c93e2ab58a5 49026 main/installer-armhf/20160630/images/SHA256SUMS b70948b0dd60cec7390d2a38b8124eae 33942 main/installer-armhf/current/images/MD5SUMS 69e0668080dd6865f9410c93e2ab58a5 49026 main/installer-armhf/current/images/SHA256SUMS 6945a44a20596ed6e81e3dea9c2f3ddf 52495 main/installer-i386/20150422/images/MD5SUMS 34ce5cc3c7b4ac330eb592aa8fb8c41e 70875 main/installer-i386/20150422/images/SHA256SUMS 1c5cc19000791e5d93bc7ffe81108eff 52495 main/installer-i386/20150718/images/MD5SUMS a0e176faa0be4cae8d8aeb3103e69b87 70875 main/installer-i386/20150718/images/SHA256SUMS 8047fd41b6469d046c4319e96a25b948 52495 main/installer-i386/20150813/images/MD5SUMS 0203c7fa623222c0de479518d7e164b1 70875 main/installer-i386/20150813/images/SHA256SUMS 7997f898ef1496cb221aebfe5c461988 52317 main/installer-i386/20150828/images/MD5SUMS 9f784e11d66674a93f119cacbc279b28 70633 main/installer-i386/20150828/images/SHA256SUMS ffe9b11bcae11587bc3923ec6523bfe6 52317 main/installer-i386/20150911/images/MD5SUMS 838890aa65f653db9ef53aea88a1582f 70633 main/installer-i386/20150911/images/SHA256SUMS 4bc3cc82cd40a2f863a1566ee59b868d 52317 main/installer-i386/20151023/images/MD5SUMS 0a41ec2e6856567d4356bda25cbda097 70633 main/installer-i386/20151023/images/SHA256SUMS 68cafe55e10bac36cfad4a57c47a89ad 52317 main/installer-i386/20160101/images/MD5SUMS 461c856624885a1979aea49ac3fd9f2d 70633 main/installer-i386/20160101/images/SHA256SUMS 0ba0a658a6f4e044805d79ae12618636 52317 main/installer-i386/20160106/images/MD5SUMS 6526fc3c4bc794aa1d005b26cf1e8dc3 70633 main/installer-i386/20160106/images/SHA256SUMS 1e77990145c4df025f08354f9c35dad5 52317 main/installer-i386/20160516+b1/images/MD5SUMS 76d42cd927e94ca61cc2e019efaaf897 70633 main/installer-i386/20160516+b1/images/SHA256SUMS 883125e6b492a708d24da1be02c8c34f 52317 main/installer-i386/20160516/images/MD5SUMS d9397411580ac3f3036df6faf381aff2 70633 main/installer-i386/20160516/images/SHA256SUMS 996eba6a5d82e580efa86b1ddd8bc6cb 52317 main/installer-i386/20160630/images/MD5SUMS 111ce06fba5d621871d271dbe4f3b514 70633 main/installer-i386/20160630/images/SHA256SUMS 996eba6a5d82e580efa86b1ddd8bc6cb 52317 main/installer-i386/current/images/MD5SUMS 111ce06fba5d621871d271dbe4f3b514 70633 main/installer-i386/current/images/SHA256SUMS d318581f7a29594cc02e0dfacb2dbfe8 2147 main/installer-kfreebsd-amd64/20150422/images/MD5SUMS 97b2421e3bde5da15598d1aa3ce795c3 3183 main/installer-kfreebsd-amd64/20150422/images/SHA256SUMS d318581f7a29594cc02e0dfacb2dbfe8 2147 main/installer-kfreebsd-amd64/current/images/MD5SUMS 97b2421e3bde5da15598d1aa3ce795c3 3183 main/installer-kfreebsd-amd64/current/images/SHA256SUMS 02c599b2311ccde70340e801d33e8483 1209 main/installer-kfreebsd-i386/20150422/images/MD5SUMS 9d81e3c19d2c88f0d3ca2cb0e06e3123 1861 main/installer-kfreebsd-i386/20150422/images/SHA256SUMS 02c599b2311ccde70340e801d33e8483 1209 main/installer-kfreebsd-i386/current/images/MD5SUMS 9d81e3c19d2c88f0d3ca2cb0e06e3123 1861 main/installer-kfreebsd-i386/current/images/SHA256SUMS 48639d5b34148741df2132cb9079bd79 940 main/installer-mips/20150422/images/MD5SUMS e592333fd6d054e559d15027b9c3ebaa 1496 main/installer-mips/20150422/images/SHA256SUMS 224d0f46f32a094f7a42295de8bc1039 937 main/installer-mips/20150718/images/MD5SUMS 55ce95ed5bbd19b1a93c8233fb5269bc 1493 main/installer-mips/20150718/images/SHA256SUMS 92be92e017b485e49883407faeee2994 937 main/installer-mips/20150813/images/MD5SUMS 71732fb124d12d18d2818285d5750cfb 1493 main/installer-mips/20150813/images/SHA256SUMS 7067906c661fbf1aa6aff1b1e3b84006 413 main/installer-mips/20150828/images/MD5SUMS 34285cf66d7d6f7ddc6eb60ec3ecf454 713 main/installer-mips/20150828/images/SHA256SUMS c2eecb82d870fab4132fb5ed53d55d05 413 main/installer-mips/20150911/images/MD5SUMS 42a45de7b7382c3be754544c384a7217 713 main/installer-mips/20150911/images/SHA256SUMS 27c4bd02410bbe9bb7ad681fe0965af1 413 main/installer-mips/20151023/images/MD5SUMS 5e0a2b483d487d151f938cc2d4a827bb 713 main/installer-mips/20151023/images/SHA256SUMS be8382a52df21fc4828ea0cd16d3e6b5 413 main/installer-mips/20160101/images/MD5SUMS 22da8c160c210653e42a34f097e94a41 713 main/installer-mips/20160101/images/SHA256SUMS f36092c5a0f5a57252aabee5235ed957 413 main/installer-mips/20160106/images/MD5SUMS ce69ee304af61fdfb6c72890929c0bbf 713 main/installer-mips/20160106/images/SHA256SUMS 419d3e461777185fd5f1dd0a098d828a 413 main/installer-mips/20160516+b1/images/MD5SUMS 042f2834caa5d1dac12d59ba54261d3d 713 main/installer-mips/20160516+b1/images/SHA256SUMS 2347e235966b2ef3d2e5a64a090a7664 413 main/installer-mips/20160516/images/MD5SUMS 83d65a4470a7bb99c20e77e542936677 713 main/installer-mips/20160516/images/SHA256SUMS ed29a35844345b187504a5b9bbd7f042 413 main/installer-mips/20160630/images/MD5SUMS a21668b1da7ae41d6be97169d2a89dd8 713 main/installer-mips/20160630/images/SHA256SUMS ed29a35844345b187504a5b9bbd7f042 413 main/installer-mips/current/images/MD5SUMS a21668b1da7ae41d6be97169d2a89dd8 713 main/installer-mips/current/images/SHA256SUMS a1c3f76716993801388b8224d75b0968 1213 main/installer-mipsel/20150422/images/MD5SUMS 266b1b098b295678ad04fba1e9734dd1 1865 main/installer-mipsel/20150422/images/SHA256SUMS 067b78e650973a61d0fc05c7789a8c16 1208 main/installer-mipsel/20150718/images/MD5SUMS 731af366b19b62a0dd2cfe33e74ce5a6 1860 main/installer-mipsel/20150718/images/SHA256SUMS 68bdb7f12ffc64f0491605bd33ef2ec2 1208 main/installer-mipsel/20150813/images/MD5SUMS 4f5951aaa4bd36dd4c6b4ecdf63f305a 1860 main/installer-mipsel/20150813/images/SHA256SUMS 144356eb6a59b11efefeccc0bd4c7f46 919 main/installer-mipsel/20150828/images/MD5SUMS d8b729cfd672eeb091b6ceb40f447123 1443 main/installer-mipsel/20150828/images/SHA256SUMS 4f178f3b0a49b9d640ef93e26bf37d0b 919 main/installer-mipsel/20150911/images/MD5SUMS 78afc840fd1494ac4245a4b533a21f6d 1443 main/installer-mipsel/20150911/images/SHA256SUMS cd31221bc19cfb52dcedbc984d153c0d 919 main/installer-mipsel/20151023/images/MD5SUMS c957ab7846cfa747a98815ba700fb33a 1443 main/installer-mipsel/20151023/images/SHA256SUMS 20ddf497b0b8ab7bab70762b17d43327 919 main/installer-mipsel/20160101/images/MD5SUMS 87aee614424b805f75fac5a08020c6a8 1443 main/installer-mipsel/20160101/images/SHA256SUMS d479dae7d216bbce0ebe88e7ef28e83f 919 main/installer-mipsel/20160106/images/MD5SUMS eea0cdfa9fa9463281705b81bb046243 1443 main/installer-mipsel/20160106/images/SHA256SUMS 96f526d410c28b3d5654677835ec32b7 919 main/installer-mipsel/20160516+b1/images/MD5SUMS c9b4ec898f63605d0b3e78c0aa3af1b6 1443 main/installer-mipsel/20160516+b1/images/SHA256SUMS 384596ffdee88d68205697cf6f294b6c 919 main/installer-mipsel/20160516/images/MD5SUMS cad28dcdc8c7b000aec4acb11bb00393 1443 main/installer-mipsel/20160516/images/SHA256SUMS 6e45f9890334ddd5952cbc9f66ff6b70 624 main/installer-mipsel/20160630/images/MD5SUMS d45a277d7403f1a21eb246a6508c294b 1020 main/installer-mipsel/20160630/images/SHA256SUMS 6e45f9890334ddd5952cbc9f66ff6b70 624 main/installer-mipsel/current/images/MD5SUMS d45a277d7403f1a21eb246a6508c294b 1020 main/installer-mipsel/current/images/SHA256SUMS 757a78a03000d563e55139d067fd3463 2128 main/installer-powerpc/20150422/images/MD5SUMS 76c2a153bd1076a232bf969c35155903 3292 main/installer-powerpc/20150422/images/SHA256SUMS 5245865eb003d4be1ed33fd642905b6f 2128 main/installer-powerpc/20150718/images/MD5SUMS 8c9ccd5536735c8f7b991c30fed75674 3292 main/installer-powerpc/20150718/images/SHA256SUMS 0580bccea0f11c44e1d57157a47aeec0 2128 main/installer-powerpc/20150813/images/MD5SUMS bdd024d098d697461fa4a96b4aebb945 3292 main/installer-powerpc/20150813/images/SHA256SUMS a4f9a02bd66f89fde4b88bc0082dde84 2128 main/installer-powerpc/20150828/images/MD5SUMS d33c3fa9fd6cc09f73ac64358d1adbbf 3292 main/installer-powerpc/20150828/images/SHA256SUMS 29a9a229be7ce7813f1c8302c8ba2cb6 2128 main/installer-powerpc/20150911/images/MD5SUMS d949aadd330e8b09cb867b28aeaa0e7a 3292 main/installer-powerpc/20150911/images/SHA256SUMS abd509be91d2158ff0670bc5a7a0949f 2128 main/installer-powerpc/20151023/images/MD5SUMS 708146f02bef4cc752e99c3859fdb95f 3292 main/installer-powerpc/20151023/images/SHA256SUMS 9ca3081166b7b2d71224257ca41007a0 2128 main/installer-powerpc/20160101/images/MD5SUMS 0085cdcca6703fdb838d06fb2cf1a42f 3292 main/installer-powerpc/20160101/images/SHA256SUMS 92cd6aec3ece889809e885dfc4669c0a 2128 main/installer-powerpc/20160106/images/MD5SUMS 1655af9c8bda75e0f89b5af81469e7fd 3292 main/installer-powerpc/20160106/images/SHA256SUMS 07e9f60a46fd4937ec6c4a7202476998 2128 main/installer-powerpc/20160516+b1/images/MD5SUMS 908bd1aa3a7fcc3904add5938081fab9 3292 main/installer-powerpc/20160516+b1/images/SHA256SUMS c4340ecd280d5db8de64db0d767e9622 2128 main/installer-powerpc/20160516/images/MD5SUMS 0f97c9252b9fb5b3c02e362b9c16d21d 3292 main/installer-powerpc/20160516/images/SHA256SUMS c8f9568908e8704b30440ce52da1ebbb 2128 main/installer-powerpc/20160630/images/MD5SUMS adb7777418ccc4e4427274ed251b7856 3292 main/installer-powerpc/20160630/images/SHA256SUMS c8f9568908e8704b30440ce52da1ebbb 2128 main/installer-powerpc/current/images/MD5SUMS adb7777418ccc4e4427274ed251b7856 3292 main/installer-powerpc/current/images/SHA256SUMS 6730181232434b5e0b07f070e4037e5b 576 main/installer-ppc64el/20150422/images/MD5SUMS 6b13d9e4427cf78bdd9a3b70ac84ff0b 972 main/installer-ppc64el/20150422/images/SHA256SUMS 595ceae6c00d35e395a3af5c655c851b 576 main/installer-ppc64el/20150718/images/MD5SUMS 9ab151980e2dd5c500a833a0a341f70e 972 main/installer-ppc64el/20150718/images/SHA256SUMS 8a5cd37d58b4647c5613e7091678a55b 576 main/installer-ppc64el/20150813/images/MD5SUMS 471ec1ca07a0feebb528f874886340c0 972 main/installer-ppc64el/20150813/images/SHA256SUMS cd63cf1e35d8ac047a67802e5dcc9cef 576 main/installer-ppc64el/20150828/images/MD5SUMS ab4431025e32c4dc83cfa311da161eaa 972 main/installer-ppc64el/20150828/images/SHA256SUMS d653d80f04396e2da59447fda908c5f7 576 main/installer-ppc64el/20150911/images/MD5SUMS 85c4644ecd578fb3ba95002dadb441c0 972 main/installer-ppc64el/20150911/images/SHA256SUMS 928c032d7073c4fb5104c1d56da6a289 576 main/installer-ppc64el/20151023/images/MD5SUMS ff02d742114719e31bfb2ad1be3970f9 972 main/installer-ppc64el/20151023/images/SHA256SUMS 6be90a72534881c30ecd59f457276204 576 main/installer-ppc64el/20160101/images/MD5SUMS 98cbf26c82a1e4406ffac22a08269b13 972 main/installer-ppc64el/20160101/images/SHA256SUMS 7b8cbfee46550eea6b2438af6ef94026 576 main/installer-ppc64el/20160106/images/MD5SUMS 701189655541d16f1218ca97f41285ac 972 main/installer-ppc64el/20160106/images/SHA256SUMS af797e06df9a57856950f7d67d4f5368 576 main/installer-ppc64el/20160516+b1/images/MD5SUMS 0a5d484587f0adb492df4b1812af9ca1 972 main/installer-ppc64el/20160516+b1/images/SHA256SUMS e89c504b16aad9e473dcb8cc3f4fbd8a 576 main/installer-ppc64el/20160516/images/MD5SUMS bce4e7471038aeba7ef43ebf15dfe7b7 972 main/installer-ppc64el/20160516/images/SHA256SUMS 2189eb589afec2cd0f0cd91089900aa5 576 main/installer-ppc64el/20160630/images/MD5SUMS d69e675fb6cd05e602bc48a5d687cd2a 972 main/installer-ppc64el/20160630/images/SHA256SUMS 2189eb589afec2cd0f0cd91089900aa5 576 main/installer-ppc64el/current/images/MD5SUMS d69e675fb6cd05e602bc48a5d687cd2a 972 main/installer-ppc64el/current/images/SHA256SUMS 9a80df21f5bd9c52f25e424e490f97bb 374 main/installer-s390x/20150422/images/MD5SUMS 20355389634104504def0522c0a4f62c 674 main/installer-s390x/20150422/images/SHA256SUMS 4e0e1ede661ac15ee62558ffc11ddec6 374 main/installer-s390x/20150718/images/MD5SUMS 4a3f0ba0fa11852a16356a02d7c65d61 674 main/installer-s390x/20150718/images/SHA256SUMS b7b0380dbafde0465b236bbefc97b084 374 main/installer-s390x/20150813/images/MD5SUMS a04317d84c2090c0b0a1000fcc6343f9 674 main/installer-s390x/20150813/images/SHA256SUMS 380794fd117148bc3e26705fe2e389dd 374 main/installer-s390x/20150828/images/MD5SUMS 21f3ba7fe04d79569e6a077c5c374553 674 main/installer-s390x/20150828/images/SHA256SUMS 31e504890d163f03b21abb6082a37de1 374 main/installer-s390x/20150911/images/MD5SUMS 6d3d1016f5df0e95773ce708b70f7bc8 674 main/installer-s390x/20150911/images/SHA256SUMS 1e2bb4157cdf43ad7f176cd58d756f1d 374 main/installer-s390x/20151023/images/MD5SUMS c0b251bb4720835ce2f25f38585f24f1 674 main/installer-s390x/20151023/images/SHA256SUMS 0ee1d4c8c85e7850d2b9c1ccf49ae5d0 374 main/installer-s390x/20160101/images/MD5SUMS 40f55c378fe0761416e3052c0db33d1f 674 main/installer-s390x/20160101/images/SHA256SUMS c1fdd09c06b8f99bf1ea705af74a11d6 374 main/installer-s390x/20160106/images/MD5SUMS 23323a568afe8645e589e5fbc4ba2e1c 674 main/installer-s390x/20160106/images/SHA256SUMS e2beb798b9b891099fe05138e6980b41 374 main/installer-s390x/20160516+b1/images/MD5SUMS 21b59fa9b484d049a30cabcc75a3556b 674 main/installer-s390x/20160516+b1/images/SHA256SUMS fe8310a09e76c93b7d32a13639efe7e0 374 main/installer-s390x/20160516/images/MD5SUMS 524f09e9c4e8910eecdb146d3de7d754 674 main/installer-s390x/20160516/images/SHA256SUMS 2355c08000e7e86685cb6c032aa38ce9 374 main/installer-s390x/20160630/images/MD5SUMS eea708fab440fdf7bce3182abe6b1376 674 main/installer-s390x/20160630/images/SHA256SUMS 2355c08000e7e86685cb6c032aa38ce9 374 main/installer-s390x/current/images/MD5SUMS eea708fab440fdf7bce3182abe6b1376 674 main/installer-s390x/current/images/SHA256SUMS 1ead3b3d8b3cb4ffb20a960556cf6df0 105 main/source/Release 0e7952ec17b7235b111d7a5016032b47 35489733 main/source/Sources f52c0103811638eb1388eeb351af4243 27910 main/source/Sources.diff/Index fa83d0a187aac7405285e3cd33aa197b 9072408 main/source/Sources.gz c64e879844fc7c8cd30f88347aae8635 6901228 main/source/Sources.xz 58aafa7955bc6322b4f57c7f5b5c3018 14518404 non-free/Contents-amd64 990b6c32148ca37b7c7aa07eb2949f1f 27416 non-free/Contents-amd64.diff/Index 6cb38c97f16f34a79db97b2bfb936a1f 812504 non-free/Contents-amd64.gz 9a08d553e6aebddee752270c37349262 13465086 non-free/Contents-arm64 f4b9c3bbbbcf805bff50e5b8f22a16e4 19512 non-free/Contents-arm64.diff/Index d1aff0dd8d4fe3a55e06387756ab0c40 731747 non-free/Contents-arm64.gz 9f70f37064a74691ff5851349343d6e7 13478582 non-free/Contents-armel 2a253ee5195c2b122c0174d3050af2de 20500 non-free/Contents-armel.diff/Index 406c5fc71cd9b16c600d88aeef6f9a43 733675 non-free/Contents-armel.gz 7f0fec44ed7dfcfe2964771b2c83525c 13539201 non-free/Contents-armhf 0a3241cb8687807a8d077340af4be04b 22476 non-free/Contents-armhf.diff/Index e7ce03d12d6ae7d25f839a034595d22a 738525 non-free/Contents-armhf.gz 4f7d7a89369a9c286e286859c0fe471b 13374121 non-free/Contents-hurd-i386 c0de361dd201bfdb7ade8971076089fb 19018 non-free/Contents-hurd-i386.diff/Index 7e6b1db1e79f35ee2be131075cb57ad4 724269 non-free/Contents-hurd-i386.gz 4e5031961c48943298aab229cc67a8c1 13755254 non-free/Contents-i386 7cd295a19aeb292d1990feed2f842d43 23464 non-free/Contents-i386.diff/Index 609a7efdd18a827fa1ee767a89da9814 757556 non-free/Contents-i386.gz 428bc6e6ca1470d39fee62c2766edd2c 13514206 non-free/Contents-kfreebsd-amd64 b022ca3c67d39f53aba5d5d5b2f31e24 19018 non-free/Contents-kfreebsd-amd64.diff/Index 9b7d361f085bfa524f03fef26692fd37 736421 non-free/Contents-kfreebsd-amd64.gz 7846ef7bc79eacff2f07ab2472b558f5 13515299 non-free/Contents-kfreebsd-i386 d839f9e53bb550ed6138f70147c856d7 19018 non-free/Contents-kfreebsd-i386.diff/Index 7b8e46a5df5e075cd606c818dd1455db 736493 non-free/Contents-kfreebsd-i386.gz a1a52318fa404fe3d51aef8281bff12b 13468449 non-free/Contents-mips e86bdef249071f965d7134d5b1e417e0 19512 non-free/Contents-mips.diff/Index 3df62209f599304456ee8665c92e0c8d 732565 non-free/Contents-mips.gz 9d3241068cc75b8480b457a51854f897 13465548 non-free/Contents-mips64el bcd527f4cf51d2aac5e8f647267b35b3 18030 non-free/Contents-mips64el.diff/Index 34703e3fbd05080c4ed72c0b99773997 731477 non-free/Contents-mips64el.gz 0fb464e5128d6bd4173a678ef5bd6c64 13479076 non-free/Contents-mipsel 4acb6639264d35253ca940fed332e59c 19512 non-free/Contents-mipsel.diff/Index cc0dc451b315c016022bdfd04b540a14 733446 non-free/Contents-mipsel.gz 6f430fbb40d213488eb2cfdadb17ade8 13467317 non-free/Contents-powerpc 489d298e3b7044f82a7f1ff0d074c131 19512 non-free/Contents-powerpc.diff/Index 07a79847ad475e8570bbb5575171bfa9 732439 non-free/Contents-powerpc.gz 72740ef0a54bf294ec73677123334a44 14067773 non-free/Contents-ppc64el 8eac145ea2dc0966125d183c8613444f 19512 non-free/Contents-ppc64el.diff/Index a953fc4a17cd0c0023c20a69638492eb 774292 non-free/Contents-ppc64el.gz 2866dc930f991ecc6cd4af049a220445 13475399 non-free/Contents-s390x f12a646b4a8c57277da1ce1a8db96ed4 20006 non-free/Contents-s390x.diff/Index 291d364ee21cee9216bd7a2f1bdf51a9 733090 non-free/Contents-s390x.gz ce45d44323ceffdbca768c2f4f25f1b0 8203337 non-free/Contents-source e1fdae378171cb5f64c6d0128d5e70f5 27796 non-free/Contents-source.diff/Index 27a2aeaebcfde059706aa256c2ae8398 878132 non-free/Contents-source.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-amd64 b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-amd64.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-arm64 b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-arm64.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-armel b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-armel.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-armhf b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-armhf.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-hurd-i386 b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-hurd-i386.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-i386 b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-i386.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-kfreebsd-amd64 b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-kfreebsd-amd64.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-kfreebsd-i386 b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-kfreebsd-i386.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-mips b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-mips.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-mips64el b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-mips64el.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-mipsel b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-mipsel.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-powerpc b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-powerpc.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-ppc64el b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-ppc64el.gz 68292125276158d468c91a58be0f30b1 451 non-free/Contents-udeb-s390x b49002146fad4eb7e4dce23bc3f48e38 271 non-free/Contents-udeb-s390x.gz a4fced351b7a9ee638a35475019f373d 194055 non-free/binary-all/Packages 84ef1554bd13e4f7265586ae3cc2a1d1 52716 non-free/binary-all/Packages.gz 1e890bc84314e880a30769b446b66e21 44872 non-free/binary-all/Packages.xz 6b4250a4eecedfd2aa185a72c35e068d 106 non-free/binary-all/Release 703be0e6bda36aa7ca226d1349c9a18b 372384 non-free/binary-amd64/Packages 07b6b19ff9fab45b2f68311fa15d051e 27796 non-free/binary-amd64/Packages.diff/Index 6d16aece55e77beb6cba6a6f0b947c09 94091 non-free/binary-amd64/Packages.gz debdd06bf579c3fb42f85f7b4268d164 78516 non-free/binary-amd64/Packages.xz 51280d23e2848a2f94270b448880dc07 108 non-free/binary-amd64/Release 602432a5f5800c8fedf44dddcbb4f2ea 228434 non-free/binary-arm64/Packages da7a4a3d93a2e9d496b20e89c327e07f 27796 non-free/binary-arm64/Packages.diff/Index 2b9040d3ce03ca155b25f3ab3e4839b6 62112 non-free/binary-arm64/Packages.gz e5452aaea83dcb29a2c5693811836e1a 52564 non-free/binary-arm64/Packages.xz 283b50d104942e828ae94f7d33cb2881 108 non-free/binary-arm64/Release 4810004f68789de4392cb741faf9bf07 235109 non-free/binary-armel/Packages 33b93ad35bf0dafb6775d54b97807b92 27796 non-free/binary-armel/Packages.diff/Index 59aea2b46f5b236f240c3f2130efaade 64174 non-free/binary-armel/Packages.gz bf1146a16f5f61261f082461b2cd28c5 54144 non-free/binary-armel/Packages.xz c3554a3861dcf3fc787aea5e23f1f059 108 non-free/binary-armel/Release 7dcb40c3bc6c3ba6fe9561b245720641 275515 non-free/binary-armhf/Packages 7429fff5c9e6d55007de8443a5505b94 27796 non-free/binary-armhf/Packages.diff/Index b529a99048903f1df03794f226c15330 71048 non-free/binary-armhf/Packages.gz a484a8e5281b93ded782820358054bbf 59664 non-free/binary-armhf/Packages.xz 994fe90c27992fac81995cd3fcc71d1d 108 non-free/binary-armhf/Release b89850fe04095051eef6fc2af4fd3db1 232894 non-free/binary-hurd-i386/Packages 661a10c10925b1352dcf45418639be97 27796 non-free/binary-hurd-i386/Packages.diff/Index ce78986f71d2a59a4f468bdb414060a0 62661 non-free/binary-hurd-i386/Packages.gz 6e420d06c496326aaca1d86c1f3fbe59 53192 non-free/binary-hurd-i386/Packages.xz f982a54eecaaff5105fc67afbd737441 112 non-free/binary-hurd-i386/Release 44b55f5cd7d84bfff6966d982b42ccdc 337604 non-free/binary-i386/Packages 6b86b3eb6dbde16ec7bd882061c73e73 27796 non-free/binary-i386/Packages.diff/Index 647a330e6b06b7702ba7c93e46ca07b0 85001 non-free/binary-i386/Packages.gz 9ea96c26fc6fda3d4e162e654a8d73e4 70972 non-free/binary-i386/Packages.xz fa526d0f76b842e864dd98295084bf1b 107 non-free/binary-i386/Release d3c299bbc70d6c7c38bf9df17344bc44 238159 non-free/binary-kfreebsd-amd64/Packages e552627a9086ad5fee01012a55bac259 27796 non-free/binary-kfreebsd-amd64/Packages.diff/Index 6389516ec9a9860892fadab427aae47b 64110 non-free/binary-kfreebsd-amd64/Packages.gz 1e9b5c2b716a5d5d1793e7383237ca8f 54172 non-free/binary-kfreebsd-amd64/Packages.xz 589a0d6328a246508c14362fffa65259 117 non-free/binary-kfreebsd-amd64/Release 91f9c8e8222d29ce84c653b11a5a56fe 238774 non-free/binary-kfreebsd-i386/Packages 69cdeb202295c8d3ec9c35f88edf4085 27796 non-free/binary-kfreebsd-i386/Packages.diff/Index 3aa0ba33f1579d5ff39de8e62dbcd7fb 64166 non-free/binary-kfreebsd-i386/Packages.gz 2465604b8cb9d3242db3c36b3be09488 54388 non-free/binary-kfreebsd-i386/Packages.xz 0b463c3a00fe3ebd875dd7795faabe17 116 non-free/binary-kfreebsd-i386/Release ac2f4e224e6abc968526cfb3f7ac32fd 232072 non-free/binary-mips/Packages 91842a9c9b311fae669060f8ba37c0fe 27796 non-free/binary-mips/Packages.diff/Index 8e3883d86f2e273c0f4489f74b95af7f 62918 non-free/binary-mips/Packages.gz 2134132b120ed581a67a235276292886 53340 non-free/binary-mips/Packages.xz a5146fc4bacdd167cc7be86231e52b89 107 non-free/binary-mips/Release 9e385da97ab1c5a0577f14e88de5e43b 229428 non-free/binary-mips64el/Packages d9e178b926025e57fb1f2347357c4416 27796 non-free/binary-mips64el/Packages.diff/Index f3afc71b380945324c27680c2522978e 62084 non-free/binary-mips64el/Packages.gz dd3217bfdcd0fad42a134b059c64b538 52544 non-free/binary-mips64el/Packages.xz 21b7cbdd20ea26dd83b31d8c3842f895 111 non-free/binary-mips64el/Release d953db2519456105ed385d3ff8ed71ce 235706 non-free/binary-mipsel/Packages 247cd4912fef0b89161407eb4b89b682 27796 non-free/binary-mipsel/Packages.diff/Index 4bf19d62515ac650088f10e64a7b5022 63921 non-free/binary-mipsel/Packages.gz 7233cb9bfec2c71e83cf1db246f09ba5 54036 non-free/binary-mipsel/Packages.xz 0f7da028e5005e5d19386284091a326e 109 non-free/binary-mipsel/Release 79f8939746dbdc7617efa22ff06d83da 231104 non-free/binary-powerpc/Packages 5377a7223ece6f27a3916ccf14c52d9d 27796 non-free/binary-powerpc/Packages.diff/Index e09d405b7aea1d80e0dbe97da2b01849 62821 non-free/binary-powerpc/Packages.gz 838370756a541c617efd97d5237652af 53196 non-free/binary-powerpc/Packages.xz 3c481bd491603b28ccef0dc33accdeac 110 non-free/binary-powerpc/Release 6fe1371819def349a4f6d6dad6309eb3 255982 non-free/binary-ppc64el/Packages ad8ded9ddda4f792725ed70e6cf77dd4 27796 non-free/binary-ppc64el/Packages.diff/Index f7e5fed4fce50cb61338d0218bac83fd 68178 non-free/binary-ppc64el/Packages.gz 640207f938532cdfcc19a3b12d587c2c 57476 non-free/binary-ppc64el/Packages.xz d4aed8973b2fdb4ebe2539a352911330 110 non-free/binary-ppc64el/Release cccf31ccee4d9552ff84ef1168890b2b 232449 non-free/binary-s390x/Packages 44b46d2a337dd0a2975fd5dd877c88cd 27796 non-free/binary-s390x/Packages.diff/Index fcf0c878aa0e389ca495d118f4df3383 63088 non-free/binary-s390x/Packages.gz eef11937550a01455d59b2b0fc465ee1 53436 non-free/binary-s390x/Packages.xz e7ceeb59c6faba0761d4bec329d3d26d 108 non-free/binary-s390x/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-all/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-all/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-all/Packages.xz 6b4250a4eecedfd2aa185a72c35e068d 106 non-free/debian-installer/binary-all/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-amd64/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-amd64/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-amd64/Packages.xz 51280d23e2848a2f94270b448880dc07 108 non-free/debian-installer/binary-amd64/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-arm64/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-arm64/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-arm64/Packages.xz 283b50d104942e828ae94f7d33cb2881 108 non-free/debian-installer/binary-arm64/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-armel/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-armel/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-armel/Packages.xz c3554a3861dcf3fc787aea5e23f1f059 108 non-free/debian-installer/binary-armel/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-armhf/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-armhf/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-armhf/Packages.xz 994fe90c27992fac81995cd3fcc71d1d 108 non-free/debian-installer/binary-armhf/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-hurd-i386/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-hurd-i386/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-hurd-i386/Packages.xz f982a54eecaaff5105fc67afbd737441 112 non-free/debian-installer/binary-hurd-i386/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-i386/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-i386/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-i386/Packages.xz fa526d0f76b842e864dd98295084bf1b 107 non-free/debian-installer/binary-i386/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-kfreebsd-amd64/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-kfreebsd-amd64/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-kfreebsd-amd64/Packages.xz 589a0d6328a246508c14362fffa65259 117 non-free/debian-installer/binary-kfreebsd-amd64/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-kfreebsd-i386/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-kfreebsd-i386/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-kfreebsd-i386/Packages.xz 0b463c3a00fe3ebd875dd7795faabe17 116 non-free/debian-installer/binary-kfreebsd-i386/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-mips/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-mips/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-mips/Packages.xz a5146fc4bacdd167cc7be86231e52b89 107 non-free/debian-installer/binary-mips/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-mips64el/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-mips64el/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-mips64el/Packages.xz 21b7cbdd20ea26dd83b31d8c3842f895 111 non-free/debian-installer/binary-mips64el/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-mipsel/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-mipsel/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-mipsel/Packages.xz 0f7da028e5005e5d19386284091a326e 109 non-free/debian-installer/binary-mipsel/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-powerpc/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-powerpc/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-powerpc/Packages.xz 3c481bd491603b28ccef0dc33accdeac 110 non-free/debian-installer/binary-powerpc/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-ppc64el/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-ppc64el/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-ppc64el/Packages.xz d4aed8973b2fdb4ebe2539a352911330 110 non-free/debian-installer/binary-ppc64el/Release d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-s390x/Packages 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-s390x/Packages.gz 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-s390x/Packages.xz e7ceeb59c6faba0761d4bec329d3d26d 108 non-free/debian-installer/binary-s390x/Release 0d8c70d4810fc47c5574ea213a9c640d 47445 non-free/dep11/Components-amd64.yml 61137cbe5792a2e5ba604e230e6b3000 7189 non-free/dep11/Components-amd64.yml.gz ff2ef29352315f747293e44ddaf8d8ff 6624 non-free/dep11/Components-amd64.yml.xz 4c111a07a096bb22ae344787a2a0b5da 45122 non-free/dep11/Components-arm64.yml 0a2b11b7f8c0ace3bda202507458239b 6383 non-free/dep11/Components-arm64.yml.gz b857ec04a99d2bd5d2fd7918d5336378 5860 non-free/dep11/Components-arm64.yml.xz 846fd7550c016f1f937f5c169ee2ef73 45122 non-free/dep11/Components-armel.yml 2b41081c0d486828af96cc9ea91df4a7 6421 non-free/dep11/Components-armel.yml.gz 771901c6f9faf59ed153e341ecc5313b 5868 non-free/dep11/Components-armel.yml.xz 1d9f53e51cd5bea731edb5b2ac54899f 45122 non-free/dep11/Components-armhf.yml f0a9a5d74b7c9b32df0bd72af83edb50 6463 non-free/dep11/Components-armhf.yml.gz 2d076be34424c23c50d059632ec94fd2 5884 non-free/dep11/Components-armhf.yml.xz f1514d4dfd5377908c95ef1b5be233d4 47903 non-free/dep11/Components-i386.yml 9889d80e20699cbb3a7e4b8b63ee34ab 7512 non-free/dep11/Components-i386.yml.gz 63ca74d5181dd5e45f1ede237192437c 6904 non-free/dep11/Components-i386.yml.xz cf0a3a824d5b36b88fa02b39e232594c 46201 non-free/dep11/Components-kfreebsd-amd64.yml 66bc6b47f324b213c3b0bc3001adc1d1 6927 non-free/dep11/Components-kfreebsd-amd64.yml.gz 8b9fd3566dfb573bdfcb33adb28a51a3 6344 non-free/dep11/Components-kfreebsd-amd64.yml.xz 04004d656d3eae5941928412bad31c9b 45122 non-free/dep11/Components-powerpc.yml 3a8feb831b6dcacb673e0ab3ccf66580 6441 non-free/dep11/Components-powerpc.yml.gz 3c5bc01d9c759c40e202dd953dbe3979 5896 non-free/dep11/Components-powerpc.yml.xz 9999642388b37c5410858bbfc5ded8cb 30720 non-free/dep11/icons-128x128.tar 9d4f8d008f7bfb01e7a0cb59c2e284f9 20291 non-free/dep11/icons-128x128.tar.gz 5591cd3a317cc78d9c2e3e9097b6c167 17920 non-free/dep11/icons-64x64.tar 3b9db0b0b6fc27c1fdfda57f249bc87c 10022 non-free/dep11/icons-64x64.tar.gz a1bf9aa6de9a10af1f4128004b077cf0 374836 non-free/i18n/Translation-en 4457f974180756e905bcb6adbc8dafc0 79968 non-free/i18n/Translation-en.bz2 207d7da0217dfe95433d5441e3e0403f 25336 non-free/i18n/Translation-en.diff/Index d90d6c59bcfbf244d26d38dca492e911 109 non-free/source/Release d4192792d76738466a698895212e1126 344837 non-free/source/Sources 109518eaae9db1e0ca222a21cf2d0e48 27796 non-free/source/Sources.diff/Index a6025e53a124caa1909653747769499b 98981 non-free/source/Sources.gz db30aa8f2a582e5a7bc17ce090390460 82868 non-free/source/Sources.xz SHA256: bf829b9d7e546bbf1db28ddf3a73e77f7f93f67b91ae20d18ee1529302414f35 1318264 contrib/Contents-amd64 ab5bca923318b59d331499317b25c30f901f2e47d31dd5cdefe0eecf25477dea 23860 contrib/Contents-amd64.diff/Index d8556a0196c369faecf88c179a69cc40c47d415cbd88fe3ed38a998dd4e48fe5 101470 contrib/Contents-amd64.gz 9b36e83393744044bc4571bbeedcb0ecb498211a754414dd9f685caa0a5732e4 1175578 contrib/Contents-arm64 7fcfe688e3a1405f84ccc52fa1c37972df1050ff4c75614d14f67437d0404c23 18448 contrib/Contents-arm64.diff/Index e6a6f730b083d16d01ff2b373e2df4c88bd3b741d9f05f4541577d5d5040687a 87797 contrib/Contents-arm64.gz 1a6477d6fd5c244477f6e677bca783d66c0e121f845eabfd286030c51cfd460a 1177832 contrib/Contents-armel 7f9f1465482feff3671cba39d3498ecb64aac75d48d2011464f3cf33f05068e6 19432 contrib/Contents-armel.diff/Index 382df57a2efbd5baf8e4d53d200c3b1052a7bd56f044cb9f3a9a7c24687ad4af 88158 contrib/Contents-armel.gz 61c5a2da5a2fe2f7c55677e028f0a16c850a2494aa23c40ee1df0a678836386b 1164602 contrib/Contents-armhf d17a8ee62e75f8b9443ca6dda19fa60a585dbc94d5fcae9d7d64a733d413fe12 19432 contrib/Contents-armhf.diff/Index 5bb87109f97302af2c0ebf4265a847a40abe9ab07f1a363ed3f9ebbe32544cd1 87806 contrib/Contents-armhf.gz 76621d859ae45809fd32e16dafcdbd9d006e9ce3e3ece25b98a8e535a2326dba 1103107 contrib/Contents-hurd-i386 d7b5b0d20ae4ccaadbd1b9e22d485d8bb03c5d4e491303b7f91f792e2d66c982 18940 contrib/Contents-hurd-i386.diff/Index 4595035baf79710a4e815a97b8a5ce958eedec839acf0f9fd916d469d29557b4 80883 contrib/Contents-hurd-i386.gz 78cac2715a0248ec8cf654e5110f868ad8150b945bbf358bcf59f9c22708bf31 1290908 contrib/Contents-i386 51577f70807bc460f53804433cc04164a0a61ec8dc5b1885a9d28285b25eb883 23368 contrib/Contents-i386.diff/Index ec1cdfa5e88caa54d1c9bca8549cc2d3750b2ec6791de88793c89b434362bcac 99126 contrib/Contents-i386.gz 9037e8faf877ef7e6896dd775d15a8e19b6ff7495ce3882d83b0782536257632 1110069 contrib/Contents-kfreebsd-amd64 a5ec69d36e16d7ee56e6efb20f2245786a656f3dd2931656a350a2e2f0dc9f5c 17956 contrib/Contents-kfreebsd-amd64.diff/Index 7f42c4aedcf65663a9b836a87fd9e3008a53f7167e08ed56221a24f46d33b48e 81795 contrib/Contents-kfreebsd-amd64.gz f9cf27931323ef075571a3dffca5dcf83977970752c7d55fc39ca791183c2b6f 1109182 contrib/Contents-kfreebsd-i386 9fdac1b056fa31a758d23e24056cb55043261399f32275f8c53de297a8507009 17464 contrib/Contents-kfreebsd-i386.diff/Index 45429180529863f1a8cb7cc764aaaa97260bfa5033327f9f7d0fedb77a3242e1 81545 contrib/Contents-kfreebsd-i386.gz a794935674b9287803b6ed7c03bdb91a1fb03b3f23219980a2fa4f8c142222b8 1159398 contrib/Contents-mips 7e81b9f46f8c9290cd83a96154b65c372680af41bbe36c8f6b25141cfdc4808a 19924 contrib/Contents-mips.diff/Index 743114afe75f74144de2e1874c711cf32ff254e7528dde43b32caf9160ec69d0 86916 contrib/Contents-mips.gz 9375ad4cad79a2e584b5b56ffbc0d37b188036337d2b9978b9e9659e9d38dd7c 1151939 contrib/Contents-mips64el 97281609deaa3ca2adb23d5e6dd31c2481ae7468b0d1818da349bac1e9f6a5d2 18940 contrib/Contents-mips64el.diff/Index 8808223dee98ff200d94afba90b7e17d5382205b63489897f5b266bd839be261 86243 contrib/Contents-mips64el.gz a7248cf41cf4f7582308b0a19faf1e140e8e0bd6d8027a9e3f609afead138532 1159421 contrib/Contents-mipsel b687657154fd666d6845dc685622831a80c9815c5740d2af7bca08633c719d0f 19924 contrib/Contents-mipsel.diff/Index fd3fea533b1368c2ad5b2f74105959b351240566ae6e739f0b183a6c268c8545 86916 contrib/Contents-mipsel.gz bb6c8927ba12948726e4b4c0f70ab8c7b8c1be973dc2f4e6b044d1ff1936967c 1180561 contrib/Contents-powerpc a99de55e3fdb31a71a6dda91e1f03a104d3e2bb0ec61cb1a6a189d2703fd8e31 18940 contrib/Contents-powerpc.diff/Index 0fabc143d764e168c95874d711c97358a224a74988760551f93d4f50733e67bc 88488 contrib/Contents-powerpc.gz 20ac4bc872eca554d3068e9a643bd0ed16fe678bbaa694e58286673f12beb71f 1142562 contrib/Contents-ppc64el 24d692bce4db68b1baf0bbeed30a8a87ef33efca469336c0c2a73c0df9f12e7c 18940 contrib/Contents-ppc64el.diff/Index f81f1599796af960f64a1ab726b01aff8023e5e6ec8f058ba9fb05906e1441d9 85835 contrib/Contents-ppc64el.gz c0807bca647b9e0f4089125fad517bcbf1d5bd28afedddc9df7492fb6428fbe0 1155432 contrib/Contents-s390x fe9b1970c85a4b42f2b3209fec732f746492ed5e2ae1ac2a36e956452a373a8a 19432 contrib/Contents-s390x.diff/Index 185ea7e3893ce2f835835d45d79984aa574f728a708ee417859932aa5f901794 86413 contrib/Contents-s390x.gz f260d59581ec8a6186a656334880144cdfbc3c6bd930191dcc9c815890259d76 3519818 contrib/Contents-source a6e0b88b478d587aba4387c9c69460bf86521ed9903a9c02c98e40c423b138ab 24352 contrib/Contents-source.diff/Index 9526c7c619de2b5ff3dd4b7f83a71bd2fc60c46c67bd687b754ace8de084a9e1 389700 contrib/Contents-source.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-amd64 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-amd64.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-arm64 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-arm64.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-armel 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-armel.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-armhf 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-armhf.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-hurd-i386 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-hurd-i386.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-i386 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-i386.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-kfreebsd-amd64 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-kfreebsd-amd64.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-kfreebsd-i386 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-kfreebsd-i386.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-mips 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-mips.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-mips64el 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-mips64el.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-mipsel 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-mipsel.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-powerpc 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-powerpc.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-ppc64el 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-ppc64el.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 contrib/Contents-udeb-s390x 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 contrib/Contents-udeb-s390x.gz 1ea1989e37e40dda9c215e652ac3c935d6649f0a0914419e35177ccf68e3ab25 90903 contrib/binary-all/Packages 463e11941e379695305bc633fd891c832180e2a71d821eea09cb514d503021d5 26745 contrib/binary-all/Packages.gz 39c2cbecebdf636b1358becfa8b06f21087572a4e3c84124df9f72efab09a926 23788 contrib/binary-all/Packages.xz ee0fc1c54e8d7dd16c06529baa5cb605ae70f9b9e2e46737973b80a2d91e6f31 105 contrib/binary-all/Release 12c0df77c229d0e18c4919b695dd9117fb1689f31530ce2e05c4bc6290777ee2 224553 contrib/binary-amd64/Packages e56d7d0cdfce75a723483600892226977743d907c377bc26104253bf62d67c18 27796 contrib/binary-amd64/Packages.diff/Index f07a086987b992444c215a60862bccbc185c60bf1316e8755abedfcedd158ccb 62636 contrib/binary-amd64/Packages.gz 0f786f1852935e2adaaf5c0e788fc5ca9f70e7727afc3305a3a0ce9a76f5a721 52468 contrib/binary-amd64/Packages.xz 71490e3ed4b8693dbff8fabba92c4269580e5c8ca917687a1179723c60e92dd3 107 contrib/binary-amd64/Release 00b5edada3ad1f90dc26a03a06abec4f1a0ee9d398b62fd9c71a05a780846710 173735 contrib/binary-arm64/Packages 5497271dccea31838151b903f242b47731dfb6c897b370604a1f614fbebf7d71 27796 contrib/binary-arm64/Packages.diff/Index f2c1ee4ef17b110475ea8bcd0efbb26ff0c60e0fc9c33e856804bd8e098ca93a 48846 contrib/binary-arm64/Packages.gz 06c36e7e8bab6ad2f3de2d659d4f4a59676e2e7a1c876c50bb0219d48b54673b 41604 contrib/binary-arm64/Packages.xz 5a270e8dea0fdec6a7ed08da01d439832f5c344909df7bc9ccb9e8ca4c81b53d 107 contrib/binary-arm64/Release b054d3a8fdf7e08073dc39f89f93b181960aace7cbca39c6ff1f8fcabcd4bdd3 175162 contrib/binary-armel/Packages ffd6f56218f57ce7f871af0265d480b013c207e6776dc79668e9da682a9a120d 27796 contrib/binary-armel/Packages.diff/Index a06d6f53c62c8b0600939389b62ba31683551e0855f6db530e5d262af1e6d045 49399 contrib/binary-armel/Packages.gz b1f0dae364c8219ba8047a15218eb03b4670892a5458673bb4b2292061338f44 42060 contrib/binary-armel/Packages.xz 6263dd4ca1f185b99ab37a1469467f45a945e04f36b448e4357643ab3d52ea75 107 contrib/binary-armel/Release def29e3d5b1a0df2266caebbebb94a6891f3565085881fd7276a8741956ed81b 181954 contrib/binary-armhf/Packages 4a0f54805991a15330df30a3ffc40e15f07d50792eceac5e9c29f2a27eb4fd5e 27796 contrib/binary-armhf/Packages.diff/Index 6d1562abdc002652a901a24e5c0da959592d856e58b4a1e1d02b1ed54110c83d 51101 contrib/binary-armhf/Packages.gz 993d0bd9f4390f9813c102dded8b535379d73132bfee180e7b85906f5d441be2 43400 contrib/binary-armhf/Packages.xz 1768bea4edde872aab0d5f086bc86bc256db503127ec434fe8d358617be1b76a 107 contrib/binary-armhf/Release d7c74c7e9a0430fa5b9eac7ba1ae935fae3687d92be253b4151c89953e634dbd 147251 contrib/binary-hurd-i386/Packages 410faf6bdd56fdbb6c86c66b710199d08eeac35309e606ff6c2070b6898c833d 27796 contrib/binary-hurd-i386/Packages.diff/Index fcf5759143512b23025e72283bb21f9ac5a534719a9b33ebcfa0be5db2b53e9b 42491 contrib/binary-hurd-i386/Packages.gz a2c012ec62047685c6c88074a28e34ff3b538936037ca0b527c26e816de75646 36440 contrib/binary-hurd-i386/Packages.xz e68963de7ec8a12061249ab755334be781d8a50ddc50748a317c73280336f569 111 contrib/binary-hurd-i386/Release 267e7aea22b22e99de2c7ba5630841c35ef141c707b08d28105613442905f91b 214181 contrib/binary-i386/Packages 4c737caa1250f6ccfacf44364cdc36549dec65377e365b1363e761e758e863af 27796 contrib/binary-i386/Packages.diff/Index a98040341e6a86f4c389dfa85c8a03dcb75c4177c46b6838c9049394f7ab809c 59731 contrib/binary-i386/Packages.gz eb80caa293ec826e4fbcd40668ee5158a467b15492b9ec9c3fb3d2818ae2cc2e 50404 contrib/binary-i386/Packages.xz 58f7a0b704e8d9ee2e832921e7c192be4787ee5f0c0d7156ff92b8d46aa452a4 106 contrib/binary-i386/Release c948929837d7911de9132656a8e52b3f7987b6e91c6c40926af92654240ade13 156547 contrib/binary-kfreebsd-amd64/Packages e66f6e99a8f7dc319b9bda3403a244a0473912a811282b2c83ca2df327de62f6 27796 contrib/binary-kfreebsd-amd64/Packages.diff/Index 7a6b4a359c792325526b74e3359c654d556d60838e7cb95bdcc3867625c5a5bb 44496 contrib/binary-kfreebsd-amd64/Packages.gz c619628473073f0dad2dea7ba6597fb75808201bca49518348b309a6482cbeb0 38132 contrib/binary-kfreebsd-amd64/Packages.xz cb57e35c4b6f12a1f7748f5a2cc4ff1716bc6b6c41e61a22b8784703716f6c1a 116 contrib/binary-kfreebsd-amd64/Release 47ce85d78a9b1e71faff4f1d824e6b00523167dd3bf0c1654984c8c9a57d06b9 156427 contrib/binary-kfreebsd-i386/Packages d503ee74a30afdc2655f6337c66e84dd4200c2f70548e4df7f61b6a74064c1ae 27796 contrib/binary-kfreebsd-i386/Packages.diff/Index 3dcaf601aee0f58d869f17485195cbde4575df497b70222b3aa5fb7d225bf139 44488 contrib/binary-kfreebsd-i386/Packages.gz 9b78795b34651bd2c1a7e70970bea1553afd625731fff10911d7324f7c87807a 38184 contrib/binary-kfreebsd-i386/Packages.xz 996020567cd141c5c6f913f468bc135dd64be57451ced8d4a05b7c392f7a455a 115 contrib/binary-kfreebsd-i386/Release de9956e00c161bbcb834192e56ccde18d71cb8a6b4118078d25f93b87cd09cb5 173496 contrib/binary-mips/Packages 5c754744e81b140f7e690c701b182e9af00a734458d370e71d54807ebdb0a2b9 27796 contrib/binary-mips/Packages.diff/Index a265202b13f18a6806b3df639675c629e27f6ddc3a05e8b9445079fd478fe4df 48888 contrib/binary-mips/Packages.gz ceb47b947b3aa7e6f487a61f6828be1ca7f3dedaa7247000bb9ca9af5e7919e5 41788 contrib/binary-mips/Packages.xz e49289714a6854e16ead5461848a9957f79ce2f0307542601af39c4c8459ce4c 106 contrib/binary-mips/Release 950648b6bedafaa9022e7e527eb1ce76fd4f60ca92e2bd7f2ae2c313bec2bf99 167584 contrib/binary-mips64el/Packages 4c598a298d38c762d63b94d40564f6a6d2f58cd30ed76603b416a456f22c8b1f 27796 contrib/binary-mips64el/Packages.diff/Index d2edf9f61acf1c2bbbb700b682279411ff18229069e2f31804e08e7941230454 47081 contrib/binary-mips64el/Packages.gz 93035259c23aca915d5870864b552f3fe76839710cdb99cd54948fc4813d9001 40424 contrib/binary-mips64el/Packages.xz 00181dfaa08ecd4abab02a610866aa334fd1a2dad4314e8faa5adcc4427ac80d 110 contrib/binary-mips64el/Release 4ac2a145c535a3943bfbdcea4be99a5a63de1642bb42811a30cb34a921851938 173896 contrib/binary-mipsel/Packages 2ea3f7225ce31cdfb039b587051ad60984f2b2c3a6bcf93377d733874ee077e1 27796 contrib/binary-mipsel/Packages.diff/Index 642ec0434e7d533a201b520346bbac9967949d5286315a4212fada7f6ecadd2d 49030 contrib/binary-mipsel/Packages.gz 274b6236427cd7d94da2d2da55d1251832a6c552a719b53cbeb8e17636d43378 41792 contrib/binary-mipsel/Packages.xz a1bd09097f80e277ad3a05af1f8428ffce1bb52e64bbe6ae3e56f8b2ac22740c 108 contrib/binary-mipsel/Release 3977d5f4dada6ee41dd0b4d4b0468014188952229eb8442d6b6752c9fb064ad0 177845 contrib/binary-powerpc/Packages cb499b93a201f701abcb3228e75941a7b0f4aa2c1cc12b9060400b0c76a0c5cc 27796 contrib/binary-powerpc/Packages.diff/Index 45205d404b8239dae53596c6a1ccf2d5043bb0ec19e586d926c1896b879bfd07 50212 contrib/binary-powerpc/Packages.gz 52b942cae4ee8188c9599f3d8afd2cd3179d270000c57d67f1e013f713ebce27 42644 contrib/binary-powerpc/Packages.xz 181b6b8ee07f298141eb500b77df63ec8acd1bd4cdecb21fd7d14d67670ff19b 109 contrib/binary-powerpc/Release e7ca4866f44abeb26a55f0f07336bf65373af386c3dd9b09d42127c20b7d4958 172740 contrib/binary-ppc64el/Packages 3b2bb1d0adbe3f2639835f5a90f9b38279134c36a2c930ddff65abba9089fc14 27796 contrib/binary-ppc64el/Packages.diff/Index 82b29a770b41b119565f463d7f484a9e6c9444a633fc11df9c823dc61247e63f 48549 contrib/binary-ppc64el/Packages.gz d1523d88aaa31a4d82199d57d318c8fe0be1cbcd3f3fc14ee823384ab072378d 41448 contrib/binary-ppc64el/Packages.xz c6338b206c1fbe67f93d3d0e27d2930c6eca65a5beff7da3424264ba555b9605 109 contrib/binary-ppc64el/Release b9a0264481d72145a597ec381b331c0bb53f7a97cca5727ee41a78558cfd8a51 170484 contrib/binary-s390x/Packages 1238a854a54996aea3fb6a5b1641c31c5aa5145abfdd8a65db061dbd6b94946f 27796 contrib/binary-s390x/Packages.diff/Index 5782269ecdcf30e029cb1dd8b2b91c50f0686d3feaac385f506a7d092c62f544 48250 contrib/binary-s390x/Packages.gz 771f6cb295fd5076a880d48131d137ebe0ff485b567baf66425766d11a98e8c9 41044 contrib/binary-s390x/Packages.xz 0aefde72db93681168f317101bc2f41a38f560db2b2589c4dbc317c5362967ce 107 contrib/binary-s390x/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-all/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-all/Packages.xz ee0fc1c54e8d7dd16c06529baa5cb605ae70f9b9e2e46737973b80a2d91e6f31 105 contrib/debian-installer/binary-all/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-amd64/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-amd64/Packages.xz 71490e3ed4b8693dbff8fabba92c4269580e5c8ca917687a1179723c60e92dd3 107 contrib/debian-installer/binary-amd64/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-arm64/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-arm64/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-arm64/Packages.xz 5a270e8dea0fdec6a7ed08da01d439832f5c344909df7bc9ccb9e8ca4c81b53d 107 contrib/debian-installer/binary-arm64/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-armel/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-armel/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-armel/Packages.xz 6263dd4ca1f185b99ab37a1469467f45a945e04f36b448e4357643ab3d52ea75 107 contrib/debian-installer/binary-armel/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-armhf/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-armhf/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-armhf/Packages.xz 1768bea4edde872aab0d5f086bc86bc256db503127ec434fe8d358617be1b76a 107 contrib/debian-installer/binary-armhf/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-hurd-i386/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-hurd-i386/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-hurd-i386/Packages.xz e68963de7ec8a12061249ab755334be781d8a50ddc50748a317c73280336f569 111 contrib/debian-installer/binary-hurd-i386/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-i386/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-i386/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-i386/Packages.xz 58f7a0b704e8d9ee2e832921e7c192be4787ee5f0c0d7156ff92b8d46aa452a4 106 contrib/debian-installer/binary-i386/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-kfreebsd-amd64/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-kfreebsd-amd64/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-kfreebsd-amd64/Packages.xz cb57e35c4b6f12a1f7748f5a2cc4ff1716bc6b6c41e61a22b8784703716f6c1a 116 contrib/debian-installer/binary-kfreebsd-amd64/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-kfreebsd-i386/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-kfreebsd-i386/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-kfreebsd-i386/Packages.xz 996020567cd141c5c6f913f468bc135dd64be57451ced8d4a05b7c392f7a455a 115 contrib/debian-installer/binary-kfreebsd-i386/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-mips/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-mips/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-mips/Packages.xz e49289714a6854e16ead5461848a9957f79ce2f0307542601af39c4c8459ce4c 106 contrib/debian-installer/binary-mips/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-mips64el/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-mips64el/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-mips64el/Packages.xz 00181dfaa08ecd4abab02a610866aa334fd1a2dad4314e8faa5adcc4427ac80d 110 contrib/debian-installer/binary-mips64el/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-mipsel/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-mipsel/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-mipsel/Packages.xz a1bd09097f80e277ad3a05af1f8428ffce1bb52e64bbe6ae3e56f8b2ac22740c 108 contrib/debian-installer/binary-mipsel/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-powerpc/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-powerpc/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-powerpc/Packages.xz 181b6b8ee07f298141eb500b77df63ec8acd1bd4cdecb21fd7d14d67670ff19b 109 contrib/debian-installer/binary-powerpc/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-ppc64el/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-ppc64el/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-ppc64el/Packages.xz c6338b206c1fbe67f93d3d0e27d2930c6eca65a5beff7da3424264ba555b9605 109 contrib/debian-installer/binary-ppc64el/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-s390x/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-s390x/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-s390x/Packages.xz 0aefde72db93681168f317101bc2f41a38f560db2b2589c4dbc317c5362967ce 107 contrib/debian-installer/binary-s390x/Release 78803324d080a7b582ee0019e2e1e2ad88b81c6deec0fa74640b3ad9c6fa239a 37465 contrib/dep11/Components-amd64.yml 4e7d5b7881a856935b6b24fa3ca0406d8481c864034b5a4a57fef15308e43d53 9645 contrib/dep11/Components-amd64.yml.gz 2061e19433c9f9636aaef0ea231dd5b3652cb599ecb293bf809bb6799d1a8034 9052 contrib/dep11/Components-amd64.yml.xz 8d42504a336fb39b0822c62ea4ad541d101396e54ba445b1e2fb08fc52bdfa7b 33085 contrib/dep11/Components-arm64.yml ddcd1c524806b5566b61b5f40c1ae4f7174c9232d417c5f60147414f18013759 8364 contrib/dep11/Components-arm64.yml.gz 44e353dfad2a5be0a64707c25a19ca07bf43f4e532ac026d348da31004906eaa 7872 contrib/dep11/Components-arm64.yml.xz 040a21a982ea92421bf97e5e09448f2f1b0aa808d9ca40acaa0d86e336627983 33085 contrib/dep11/Components-armel.yml 332ffa4734e92ad7c8feaf398473517b5058b86e99736b5e42692ddbd7771303 8255 contrib/dep11/Components-armel.yml.gz e5c0382a448aec0f5118c3d8d82eb408196ca394959689bac38a44b6b4f2b5ed 7832 contrib/dep11/Components-armel.yml.xz 79e3101f5d6491a7a49ff98323498aaa470d260a0edd04021c8f8cdd6d145b0b 33085 contrib/dep11/Components-armhf.yml c7e6ce867f6b68ccabfdb28be189401578aa5f0c2670c8164cfcf0afd050d724 8344 contrib/dep11/Components-armhf.yml.gz d03bb387c09e6ba87b3081ec501be351345e9232f7f8fe0025fef8beeaf52d36 7860 contrib/dep11/Components-armhf.yml.xz 437e11772ff3b605e6fb72637964ab1f87890dbcb09a24eda6604d538e7cfad8 37465 contrib/dep11/Components-i386.yml a191ae6e249fc6cc2a36b9a8d099c00f896e5b199ea9db9e42a587d63aa130da 9620 contrib/dep11/Components-i386.yml.gz 65823984f864de6cbb01eca72114d9aa5452f94489bb2d44fb14fc4a15ae344c 9052 contrib/dep11/Components-i386.yml.xz 1350606ca684e6b33c10baba53b221417658e6e874dc7dd0c8ba23bf3d0e2cb7 31558 contrib/dep11/Components-kfreebsd-amd64.yml 5aa324f0a3d86754f615547f81a3098946dad126898a114bf3384b1aa25ae2b2 7777 contrib/dep11/Components-kfreebsd-amd64.yml.gz 8ba91b03d935a3c1b27b2502d4b84fd8f4802798a1306ecf9e5f7d31681a8b89 7328 contrib/dep11/Components-kfreebsd-amd64.yml.xz b6f4ec3d75799ba55e273013e16c565435d22cec1f953850ab25b255c894c1e6 33085 contrib/dep11/Components-powerpc.yml 70738901d0e9380795c42f8912a75255875182e85e29f3f0653994a0d37620bf 8314 contrib/dep11/Components-powerpc.yml.gz b372cd613418c5aa2c007c769681d371bcf9d1b9d64f9589f5ee8c08ad1bbb50 7864 contrib/dep11/Components-powerpc.yml.xz 7212d818f92be37551ab43b69c5e8407dbb1fc6d284b87a499f9f2ba2ebe6c46 372224 contrib/dep11/icons-128x128.tar ae92d6c1fad90050aacc63f8ec533470b997b7a3ae1ec2a52eba6dda005218ef 304821 contrib/dep11/icons-128x128.tar.gz c7ad3e1be349e072e5573ca6af62e2ac39d37736a055fc95f3f16b1798690ce5 160768 contrib/dep11/icons-64x64.tar b9c9334927887f5fd32145190e9bd95a36591f93da1ac193a8128c4a20943c0e 120717 contrib/dep11/icons-64x64.tar.gz da44e12b5a6d7558895de021e780837656f411fea9c88b4255701916b947a936 180882 contrib/i18n/Translation-en 6d16a7bc3696e35f6479ed18ba8e90c3b9dffe2147f81d02707f3427b80f84a2 47717 contrib/i18n/Translation-en.bz2 69dec5274c95b0631e486fd3d648df374c1dccce066188db40ba97e0a822c645 12544 contrib/i18n/Translation-en.diff/Index e403c8d0095bcf93eef7d4360dbdb20089048b68afab3d91426ff1e2c8f48d76 108 contrib/source/Release dc48b06a2e96795f1a6f4c45f56c240b67e565f66ce61582bdfd82cf7e17ae5f 189731 contrib/source/Sources ff30ad72c4c4b7570435ae32862f412eaa3c90d3c6985205287ce5c1e38a8eb1 27796 contrib/source/Sources.diff/Index 2a18ad43b0b5c24a64131651e4e7c18dc7b24bdbd9172b05071e6681cf0e21b2 56034 contrib/source/Sources.gz a1548fce14a6d0ac8b2e8aa86ac38a5d90f1b5cb9bacc0ea8592bb321ff57573 47676 contrib/source/Sources.xz 6225e8292f73d39299cfbd47f3b4254ec305a88b2370deafaf1f8592eb52d124 486486404 main/Contents-amd64 b2775e7db3cc971e7df8b51fc12c0dec3ac241cb1f8664d11c04b6d0f546dba2 28024 main/Contents-amd64.diff/Index 098b4d2ddc6c0f72a0e0a326e1f3773df7f836a6aab1022631c2c69c3c4669d0 33741709 main/Contents-amd64.gz 4f2d4cba3efb61f71756101e3c70a74a0d77caf3c2b394dede1eafef6405c325 464406406 main/Contents-arm64 9b8ddf71ca6d99bb0704d498913c899a4e2c1806d08c5ffdecf74c87138fb057 28024 main/Contents-arm64.diff/Index 7e9e7275a41953053551debbf21da8c65679e314b4932f12057f81345872f8c0 32427484 main/Contents-arm64.gz 8230fa913e1e95058d2916706f7112a5847c93f881a217f5feb878360fadb74e 462714353 main/Contents-armel 6fa0fda7a0e268394e70af0b5cbf80402b68cc0782caabef8952cdc6175a061a 28024 main/Contents-armel.diff/Index c1f16fbf62d2e33ffaf7494e23dc48d39717d0c5d0eb90a41a0de45eff8599c5 32372160 main/Contents-armel.gz a1737895549660f27229c2e0d519ebb3e73409f0000a3524952793bcdb7a49cc 465331124 main/Contents-armhf d4ca75f069bd2a71ea4e9466bf9303504b5b97c0308e6f34c2324d325d5c9f68 28024 main/Contents-armhf.diff/Index 338ba9c6ba391348a0f27ab8221b79d80124fd45177a4778d98b4cd034e5b6a0 32551542 main/Contents-armhf.gz 12164d6ca2768ee4eada8214439ae38099507007ca58594fe3f26d5a8c7d17fb 437011535 main/Contents-hurd-i386 d7350f21962e0039b025a8617603732410c75833e3f3237d58816b0d02bdf1ca 28024 main/Contents-hurd-i386.diff/Index 42d44861b008f6db216c3a3ba6567998fceaadad5942242c731549a8da7df6f0 30282285 main/Contents-hurd-i386.gz 7539740c4b23e36bd0eaa28932fe789ee0b3da2655b2d70a92f564e5b17dd703 484515088 main/Contents-i386 b45ff8daf9be2d182cdba5f19a61e9e0f33a51fef65d38c73cd423892ec1e0aa 28024 main/Contents-i386.diff/Index 90263a4a8f933e0ac015a657c2f02398d003380248f2289e5c4ce5753e206a61 33649417 main/Contents-i386.gz bdcba9995f0782c9243f7f735172485c09719357cce8a15a243827ac3d3ae750 449136892 main/Contents-kfreebsd-amd64 49a32acaf40b0e20393786fda93abdce653df535f8b5cb76eed693912d4bde40 28024 main/Contents-kfreebsd-amd64.diff/Index de09420766148813556cbbad3871290a355a67a28b1ab63948ebbddaea6192b5 31341686 main/Contents-kfreebsd-amd64.gz 75ddb564e556de94bbea1bdfc368ff38011e49eec4f4d8834c6b99a722549e76 448627460 main/Contents-kfreebsd-i386 3976553adcaf1deeb11acd7007769542a27c88b557867eeaf7abf0714f22a80e 28024 main/Contents-kfreebsd-i386.diff/Index e932f6b3a78ce076c567e402ff94a6f9d760015ea3ded01c8ca191d94c745e0f 31305004 main/Contents-kfreebsd-i386.gz a15462d0ceb1767fa8ec6e417892435ac9aff42270aed4f368eeeb7a80f9159b 460036580 main/Contents-mips 321f2afa5a49320dd1eaecf1faeec7686d7a1770272be2ef5a8ce6265791812f 28024 main/Contents-mips.diff/Index f894fbe0c5b6a5b9b166faa760402debab16abc724e2449dfd13fe5023e3ca19 32236221 main/Contents-mips.gz 5198504cf1241a5f6ca4ae597b9a8b1c2cf8fa59385f50cd8afcb21034c66971 459438445 main/Contents-mips64el 47d5a126188417ed83fb74c096e3e503a9d0bc3756bc1f92144f7eaa5a515791 28024 main/Contents-mips64el.diff/Index 8ee370da3a1e6b9a3f7632a41e5d8c54a605268baf5431906e62edd4359e048d 32030589 main/Contents-mips64el.gz ca351edb83bbc6173e4f54329ea358d52fc7a8a5da14a29ececc87d58f960e19 465041084 main/Contents-mipsel 603df43e401e4a0eb1f4a2c71b111925d37604eabedb33742b756eb928a8414d 28024 main/Contents-mipsel.diff/Index 8bf4724562bb6da2a84c536a1c81215f60c2f3c824d44e23abe34b35b5f7c96d 32541643 main/Contents-mipsel.gz 9ace3630938979046fdd74a844369edcb1f3ef2189cba2b31802cb20a3f51ac6 465654055 main/Contents-powerpc 0715a66ffe4877045aea9fa219b383ae9f4885d8240f0c09eca075aa52c7facc 28024 main/Contents-powerpc.diff/Index d407db7ddf9defebff35daf0d7cdb8af7b3a09bf5ba250b00133dd9f1adcf38b 32538984 main/Contents-powerpc.gz 71f19e382c618b8718ec6e7b2ef852d9b9410879010d15f4e2dcc29e74ce5291 462882892 main/Contents-ppc64el a117a8b76916ae3f5abb0fbe4d782892ece677b9880e81c54f24b1231d89e26c 28024 main/Contents-ppc64el.diff/Index 6299082dabeeff4059fc802447ec43ef7cdae2f9072b7e83d9b503092aff8873 32352649 main/Contents-ppc64el.gz c96502c6d78173e932c1998414d753ecf19e4e903f273360834e4b981857722f 459302164 main/Contents-s390x ef600d65d3c84bbf294819c62d7e586b62ee84b3c65a3b3fdc1a9062e69abd28 28024 main/Contents-s390x.diff/Index bd69285fb34ace9eff6e059d94b4f5db47d2ff2c4c4b68deeb352797f476618a 32174741 main/Contents-s390x.gz 151abe91c34c7ba0f9d7ea77bf3d447712dfa558c6d903427b0b56c498067049 482453699 main/Contents-source ec81b1518bda86285179b3a39914fd23946572031b687206d63106b97b92f8df 28024 main/Contents-source.diff/Index 2f38e5ed5a80d3ab28db61cd585b957bf4a53937682dccfbfe76c076dde0a311 53745770 main/Contents-source.gz 87b1f789988db6646307ce219b6913aef080672629c6c838d3e7023123e7937e 484751 main/Contents-udeb-amd64 af8ee15ec674e995fd85d9b5cfbc781b00af18c3267f19a8c43d00b1f5710741 38535 main/Contents-udeb-amd64.gz 119c16a23678425b7d07faa91740d81c3cf33a7112f3574b34597319b7773a61 419372 main/Contents-udeb-arm64 c964c023c67a2557ccf91b163b6662e199b35ab4c553b11e1169df31da5460df 34115 main/Contents-udeb-arm64.gz 630cb57f948c63dcd57b173b1e587c3daf3f072b625efda5bd6e47314ab6f5d9 414623 main/Contents-udeb-armel 3fb372ba3c72606f9ba433c20a0f3be6279d86135b174965335101cee6a2515a 32491 main/Contents-udeb-armel.gz 2948576d7aff0f15b6d3c2b274661cad417c1ca5e86618a646d22df9b48bb8ed 504043 main/Contents-udeb-armhf 1a4f163635930d02305aa4f52125da4b28a2de5728efb542d674810e5e75d521 39965 main/Contents-udeb-armhf.gz b292917984474cf3a7d58273af21949eed87a1c2e88a0346a9315046b375dbad 266208 main/Contents-udeb-hurd-i386 f935fd65dd1de5de4a8d3b2cdc55d47fa27e70aaa44efc66252adad89d2a8a4a 22517 main/Contents-udeb-hurd-i386.gz 998617b62e2cd1cfc818d0c4d07e07b2c5942c1cedefb699569d779c6f95000f 710802 main/Contents-udeb-i386 7cc1a2777814d21705cac5f5bc4229b7dba2aaa69b29d088bc73d2619ee67a80 54082 main/Contents-udeb-i386.gz a948334097278469ad5bf3dc366f8f525d7ebddb0ffae8faecf3827ef67ef24d 289951 main/Contents-udeb-kfreebsd-amd64 55c62e343faa3f8cb5f5ff4df5085fa05feafdcdbb0ccecde2a4fb89b980ed35 24182 main/Contents-udeb-kfreebsd-amd64.gz a9e7d3a6342280744a286310ee0b9ae048f56b329896f2909f376a97a39f30ab 289869 main/Contents-udeb-kfreebsd-i386 740e1b1712b39d3dbbc2889708b0f933b5d728a61d8a5866217075769d907f87 24244 main/Contents-udeb-kfreebsd-i386.gz 5a49f76c89934dccc1b80ba58aad231bacfd7263f4be50de1b74c9db725275ad 577017 main/Contents-udeb-mips 928fa491edc0df94fab8430f5b268542f057b312c28c9487507b2dda68b43d41 44213 main/Contents-udeb-mips.gz 047c1d01fdd2a46d26731c277ed0bc1c7d5bf17846b02ad394ff0b5e47202425 723583 main/Contents-udeb-mips64el 501ebe45aa5e0b6868bfc10702d14b40b5c0fc7ae77e2aeaa28b78ea7ed62204 53323 main/Contents-udeb-mips64el.gz d11db1a1372711ac5b0cd4f100176b9cd6ed9f61165b108a674f1d44d7dc4119 1008989 main/Contents-udeb-mipsel baec12d4d9cc94be881b304becda72a351b5945360631c9dc548cb8a59e02a67 71028 main/Contents-udeb-mipsel.gz 3c2c2365976c953d4d3197688f0f93a3e5801930f17c3069d67e4f4c6d845459 583828 main/Contents-udeb-powerpc 05a72e24f60d033fa4129501b838382a1ed6f10e49bedc813430a6484d4305bd 43594 main/Contents-udeb-powerpc.gz 645c1ffb53890a79a889ce0620a26497db79ea26258d739a2c0a116ba482ac53 396155 main/Contents-udeb-ppc64el b978f738e7d73e9368a69eb28d6b316a985e65bc5d2b6e8c470b002604b397d2 31809 main/Contents-udeb-ppc64el.gz 2f25f40a1fc13ccd5facc9a6af3821ba04dce9eb4dadab15ac4764437bf83bd8 307502 main/Contents-udeb-s390x 825cead37f987b4432724197cd8cc95cbd33e86c5da6b18b3ad94fa7bf11c353 26131 main/Contents-udeb-s390x.gz a852b64970a8ffe3b26ea70c9672af5b8ad2f135ba404b88606509568d8b527e 17781690 main/binary-all/Packages d8d49a3ea4ffed502df05687f1aadb88f59810c5365bb3c2634d32b793278126 4391664 main/binary-all/Packages.gz fc36847abbd1e503068c1fd933490d4d2d1088e4ddca65f771ff5daec226c121 3337812 main/binary-all/Packages.xz 426d4a8e95369e5d97171b4e8e5dd73a1039231d256e35cfd223bf572be116ed 102 main/binary-all/Release e7a8f1dd5b54a1600a4bd3505e9f4ded10cd3017f47dd79e3aa0e4eb102b788f 39256459 main/binary-amd64/Packages cc5ec05a29811cac40dc601b31c926063061d985ae83bda66b27b4109ecc0d8d 27910 main/binary-amd64/Packages.diff/Index fa0a858a78634103379a48636ad32983e253bf693488519f59bd82d231d89e64 9576288 main/binary-amd64/Packages.gz 24f6cf9b24e3d5f10b297dcc25c12e323258130625e3ec7de51419b66d2350b4 7147528 main/binary-amd64/Packages.xz 07cb692d133dce21dabb9e42201b8e632a7c6a069b53f9ae8173a4ea24606da9 104 main/binary-amd64/Release aea1f7ba403d6f839517644d48f224cbfcc2124f14825f25b93bb7116ca46e57 37571520 main/binary-arm64/Packages 5ac35ae5d5d97ccd2c662fe3a52f16cf59703f534fe254f8185582827aad1ab7 27910 main/binary-arm64/Packages.diff/Index a24fa93bdd882e9b860957b7411b8230ad59030cb3e8a7a2ffe40fe20ff40e13 9227318 main/binary-arm64/Packages.gz ee29d1cce5817c25376ba3319d359d370c8122ec842518d4a78f7195c0887068 6884520 main/binary-arm64/Packages.xz 16b43705f8420470a43f96954eb7d00a75c4408a36c1b86ccf2bd6cfa4ebfd98 104 main/binary-arm64/Release 5518a06473c0cd4edd4ccf12fc4d56397571887e3817b3c86186a1a833579e16 37699046 main/binary-armel/Packages 63c0ef80d23c66ddf5cc78d76a0662b40b72cd379f163817b9aaaad6ee3f989f 27910 main/binary-armel/Packages.diff/Index 1aac32bbb9e5bf5f4cf7b99034611cd8ec1134a75ec4897fac529f29b30d933d 9275300 main/binary-armel/Packages.gz 94e5a90f20410fb4d87451bb5ad5fe363e418664324ffedd809b00db2deb30f3 6921736 main/binary-armel/Packages.xz 82b62056193684651c2bf9ceb2af527cc75d034576d4cb3bc9b2f95fabf93e8b 104 main/binary-armel/Release 5764f36e2223186bff0fc3306900ccad92e8e8b8b705b3d289f5d1c5898f2e37 37788052 main/binary-armhf/Packages fc39c2a13723ce15ed7bd1323c625eefe63708106d313d0c26bfe303c99bf4b4 27910 main/binary-armhf/Packages.diff/Index 73614cd887087b115a5d51f5d4d69a8f56f56b8a51d8e2231f8b03c8b7010a3c 9300334 main/binary-armhf/Packages.gz c11f25f4cf6b37b33ba284ee31a960c230a3213e557277e3cd2382a722791e02 6934520 main/binary-armhf/Packages.xz bde8b39b284457d38e4af1f04f9de8e73a8705795de39525a6def7d42e523396 104 main/binary-armhf/Release a4aabc159ce2439c60a0f595e8bb474303966d85bd37a880e4c93c86a47c07d1 33806130 main/binary-hurd-i386/Packages de4dc855e795cf6e73d8d9dc1bc6f9d6cf523bb9b653f6bbcdbea1fea25cadc0 27910 main/binary-hurd-i386/Packages.diff/Index a1812e288764d013f66c5b9baf3d9a895cfe1f6152c806ceb72d8394ae1d1ed0 8234995 main/binary-hurd-i386/Packages.gz 0ab729453f01a564261190614151fa3391261815ef771cc1c1278b9ea4986548 6163664 main/binary-hurd-i386/Packages.xz 141c84b75fc4356d505e86c6603dfe15875c7be89f23b4e99570b97822110b67 108 main/binary-hurd-i386/Release 96fdb290724544ab3ed6f34563d96b13054b3ec219ed84588ec32e233bb1b172 39044462 main/binary-i386/Packages d1a7007422124bf288ac478adfc7e32c6db4f4cf7e84cfb9e07aa035fe302c1a 27910 main/binary-i386/Packages.diff/Index 0424e96da42d89dc31b7a2e88ad7b34fb0d48c1943b109dcca3abeb148794d0f 9548697 main/binary-i386/Packages.gz f121096021939484b9f54001067af8a686291e31ad98968978ef7b54036762d6 7122292 main/binary-i386/Packages.xz fd5827463ef11fa7a4c0777a83766441d7a730109e012830e7ded5aa051042f6 103 main/binary-i386/Release f8ee3c831a70de7db68cb615ac5c886398a5025604ad4f58afd1f806e7c1eed9 35913320 main/binary-kfreebsd-amd64/Packages 5ea30ce949672beacb096f8dc16c99052448254c10dd459b7579dec03bf543a1 27910 main/binary-kfreebsd-amd64/Packages.diff/Index 2b9827f5561975831e609fd73aa6f305fe46848a6f8989fe951aa4cde84ec60f 8718641 main/binary-kfreebsd-amd64/Packages.gz 93a4d574ad7d1c7d430698b2fcbe7a390b663d9855339075d8b39f3fd03858a5 6512728 main/binary-kfreebsd-amd64/Packages.xz e9cb26effa2842679d46dea78abf7662b5a75320e0dcf15054164abee90df89e 113 main/binary-kfreebsd-amd64/Release f61f0a57cd6e3b90f29df78ba6fc5936fa930f58df4c44eecca39b5ee77fe844 35809649 main/binary-kfreebsd-i386/Packages 4c076575c95105c382338d83dbd5021c17b10123f2c25fd957a83109a1cf0c55 27910 main/binary-kfreebsd-i386/Packages.diff/Index 62ffaa506a24593028cfc049178c129316eec91611dec632e8dda7c6fbe00e38 8704036 main/binary-kfreebsd-i386/Packages.gz f5a48fe4984d5db55e638cd42474d5eaa1067b8bb124fd6e2564209258033c77 6502680 main/binary-kfreebsd-i386/Packages.xz 0f5494dc8749cf8141c7f288273c275f7ee50c1b37231418f1db376bb7478b83 112 main/binary-kfreebsd-i386/Release 410e9b3fa8a7b06d40de71111c99aba20c0aae3df9c4fc26adbfb9092ade2c87 37366754 main/binary-mips/Packages b1b87da67376841c606df73e2db8b4be71cd3b9b2ba7a84bf281e92ad7445604 27910 main/binary-mips/Packages.diff/Index d070dfa70e6aa55836fd4855d25999dd1056c3f59cd227fed0b11f2e8cf293b8 9210212 main/binary-mips/Packages.gz 7624f9dc42e9f09da0b34e7b41812f0f38ab8425c7ea746d08a3731dff3d8a4e 6873512 main/binary-mips/Packages.xz cfdd9ddbaed505370e4b7054ada267efee4b38acb2eede584efc776ca12ef90e 103 main/binary-mips/Release 15974180876dca292aa33a93bf4712c579e48a50842020fc9fbb5bf6185bebe6 37340330 main/binary-mips64el/Packages d7723c2e3a0d4931f22dcac54bbcedc697b22b5cf0bb690216acfefd4f69944b 27910 main/binary-mips64el/Packages.diff/Index 32dff20bc482f0a3a2e7117c9b31908985c7da4065a610327e9d04c3bebcc712 9156063 main/binary-mips64el/Packages.gz a5fe219c492a1d1af2e9e617c7a16c962f7fc8fdc99fadfd8e786349beac525f 6834608 main/binary-mips64el/Packages.xz a3135889946204b9cdb22c8f2f435e566f1fd941606a6666b0297adc3a8baae4 107 main/binary-mips64el/Release 40f282635f2bde397acaf3e76112306552adee959c01c804173dc81a6c4564c5 37668023 main/binary-mipsel/Packages f6614536a8dca5f0c5e181cf2e60739327693573ac5ea20f33805ca96409bfde 27910 main/binary-mipsel/Packages.diff/Index 2e03fbe6ff2d29d7c3e20ba5b50f1a172c5dbe708d1b9d5b3bb5ec7f49acad40 9262851 main/binary-mipsel/Packages.gz 1e5d0eed7c2582cd21a58d5db2a6bdde2c1ba62c5c88a68665a06a64e32fcdee 6912400 main/binary-mipsel/Packages.xz a4c0b9ba49c406bfdb0d564e7daaf8b2d4e0cf978ca37b2981fc970d68557dcb 105 main/binary-mipsel/Release 3e600b48258b911254f8e12d2eba516f0c86bb6cb66bd07afdb1a222dd6e59c2 37823973 main/binary-powerpc/Packages 8ddf615b5f1b079258803a4fafbaec237ea62a6f9095383a7092320b82bc43de 27910 main/binary-powerpc/Packages.diff/Index 43896af9beb88d69a64e1f97aed98f2b8a383ae8b102e23414afc6638a98427a 9292207 main/binary-powerpc/Packages.gz 6d69e6f6a2fd87ce8f704e3368f92a054a2b52c2457ec69cf363789e24626435 6929276 main/binary-powerpc/Packages.xz e58934ec639194e73fefb1d44b0bf5866adce147652d74126c5f401691241e6d 106 main/binary-powerpc/Release 43996548337ad75ef7a404d90fd1d32376882559ff019f78012e71b1ac64d83d 37859587 main/binary-ppc64el/Packages 3515efeaa92655d1c1a0ffe582b21db9fa288afc425b69a4e9f4c474817aa857 27910 main/binary-ppc64el/Packages.diff/Index df0bce791228021b5c44e31ec8256625627cf1ecab2d747e3fe1bc6877051b87 9277244 main/binary-ppc64el/Packages.gz 3c85bc4f712205f3445b4421068a98eb69f203abcd4601efc659eaa1bcb8416b 6921148 main/binary-ppc64el/Packages.xz 29eaee9b665539956ef860f083f3885b3d69145a8c9bd33dba701039a2c46e00 106 main/binary-ppc64el/Release 7e26f1ac0fc0a166e1db5702246cba50ae918855b5ec8fa7272ba231e018a5af 37432240 main/binary-s390x/Packages a76c3178898c41ff2d7a9e95a5a105bce1ee7bcf564824ced543abc06990ec23 27910 main/binary-s390x/Packages.diff/Index da4b84c571306ffd758aae45b48037168fc87f8d15842174970266bbdb807631 9217612 main/binary-s390x/Packages.gz 69d9a47f563df54153c018c0f2f705248a97bed26971c981aae34780de38d69f 6875604 main/binary-s390x/Packages.xz d9226f019e4de7c67111161156d98d7bff3eb72164e69532c0e9066b2a625f7c 104 main/binary-s390x/Release 19dcb29f8d2240f8e95dbd97b1dcd1ac1ed7429f906f4f2ac6c929fa4c42fb25 63066 main/debian-installer/binary-all/Packages ff8347481ac62bd1a14c89640b3c65e753b07d734b2747b2baa1350bdc066ed7 16829 main/debian-installer/binary-all/Packages.gz 2769d338207e4fbfaf84e9ad4ad2e39cb52940cb154529152295175e86f24034 14836 main/debian-installer/binary-all/Packages.xz 426d4a8e95369e5d97171b4e8e5dd73a1039231d256e35cfd223bf572be116ed 102 main/debian-installer/binary-all/Release eafb7d2b5be831f402929d6aadafd785db65240cd471782e08694c2b78b3b00e 258901 main/debian-installer/binary-amd64/Packages adf69b6aa09e4df452c71acca288349159c3cf8309912aaeb1886b784471f331 65129 main/debian-installer/binary-amd64/Packages.gz 055f2922bdddab4bc5c0e0b72c1835c6f657c2dcbae8add4f85fe5862e3557bd 54808 main/debian-installer/binary-amd64/Packages.xz 07cb692d133dce21dabb9e42201b8e632a7c6a069b53f9ae8173a4ea24606da9 104 main/debian-installer/binary-amd64/Release b831881dba6b4a110a45450f7e8f6ebae2d700db99fe3c5444674a1e4eda6de3 239565 main/debian-installer/binary-arm64/Packages 50b92d50e650200ccd609893a9718db46ff78c65ca4668450a7efbbe32fa795e 61678 main/debian-installer/binary-arm64/Packages.gz e936937fba91eeb63e66703cf817a8b9c65024f49e7fa86f25f7f4639b33d78d 52152 main/debian-installer/binary-arm64/Packages.xz 16b43705f8420470a43f96954eb7d00a75c4408a36c1b86ccf2bd6cfa4ebfd98 104 main/debian-installer/binary-arm64/Release ce5a93085e974dab2deb0da4cca6e5d0858d76bf310811dcf5370bef20883861 277695 main/debian-installer/binary-armel/Packages 5a44faeca117a93ab177834d891e43263345c213b8c08a822c08cb02151f8bcb 67415 main/debian-installer/binary-armel/Packages.gz db01006a7f689c09f0a451c73a15bc93a4c2d4278c7f5492e70691a02de7200c 56652 main/debian-installer/binary-armel/Packages.xz 82b62056193684651c2bf9ceb2af527cc75d034576d4cb3bc9b2f95fabf93e8b 104 main/debian-installer/binary-armel/Release d6d1f8df525dd572f9bf76d102e8c7e4e1e2ac1543662b277f4399f0817a8908 238490 main/debian-installer/binary-armhf/Packages b91d5f3c91a66cd7f55b640dec1c8a6b69379bf85eb1c98840d165289abf9bf5 61449 main/debian-installer/binary-armhf/Packages.gz e785f3eea00be33e3726b650b0f77458833d150e7ab64adb86596b21b1c39322 51968 main/debian-installer/binary-armhf/Packages.xz bde8b39b284457d38e4af1f04f9de8e73a8705795de39525a6def7d42e523396 104 main/debian-installer/binary-armhf/Release 52ac986b0e26d384e51c43a2bc59afcc5736405ae1aba47ea32aead426f47821 163351 main/debian-installer/binary-hurd-i386/Packages 60540e4dc290c9d578d0a6bde73f41394cf2deaaf57930b0e889b5d1d07dba5e 44433 main/debian-installer/binary-hurd-i386/Packages.gz 3c731885a85e3ad6b43256c3597922f64f4c3568cece7a928613d9cf2bfcc56e 37804 main/debian-installer/binary-hurd-i386/Packages.xz 141c84b75fc4356d505e86c6603dfe15875c7be89f23b4e99570b97822110b67 108 main/debian-installer/binary-hurd-i386/Release 3423ee13dd694bf8ae4e612b3776ef8db21dd79b19285f3cc11f47a93007e01a 326037 main/debian-installer/binary-i386/Packages 30faeea1e84e05189cf958c17a73c779cc20bbaad5efc372ba00f0a198c4ddbe 75140 main/debian-installer/binary-i386/Packages.gz ce7f378714cb417823c41c8eb45a4ef34e9363f35e743448dc6d902fca8770b7 63056 main/debian-installer/binary-i386/Packages.xz fd5827463ef11fa7a4c0777a83766441d7a730109e012830e7ded5aa051042f6 103 main/debian-installer/binary-i386/Release 8613a8e66cec2c8ea9ecc65439cc12f7378e3c3c106200dc7b890c4e97160f0d 199536 main/debian-installer/binary-kfreebsd-amd64/Packages dd6dbe97ddd9e1b3ffd98d7bd735d138f8272d2e8150e3889d95cda89401cf6e 52052 main/debian-installer/binary-kfreebsd-amd64/Packages.gz 498d55152dbf9e3a5ffd0e16fab7d9056252f6696fbaad0965bfa5328616aded 44020 main/debian-installer/binary-kfreebsd-amd64/Packages.xz e9cb26effa2842679d46dea78abf7662b5a75320e0dcf15054164abee90df89e 113 main/debian-installer/binary-kfreebsd-amd64/Release c568de8a747cdb5f9287b5a00e44363e97c8d4663fc45290b517a2a593978d7a 198799 main/debian-installer/binary-kfreebsd-i386/Packages 4d91b1cf5d13db9906a0db7346f6c5ea8f3faaf5ae843042804b1092801e8953 51848 main/debian-installer/binary-kfreebsd-i386/Packages.gz 88d289b4910806a472adf4d5d40a60e39753c3a5a42a4fca7e1c14ac5c5ebfec 43944 main/debian-installer/binary-kfreebsd-i386/Packages.xz 0f5494dc8749cf8141c7f288273c275f7ee50c1b37231418f1db376bb7478b83 112 main/debian-installer/binary-kfreebsd-i386/Release 3adcece3efd312cc584c57667b5699edb31c7d9feddead3724d789b32e4caed8 299343 main/debian-installer/binary-mips/Packages 80a6228f3e5e5c4378f3ca3c4a065f7eae13eb071714f58f54f71334acd7a022 70317 main/debian-installer/binary-mips/Packages.gz 762c50cd552ee6628629909f708b393f56a9be0ef84fe8658fb8f74f19e8d875 59428 main/debian-installer/binary-mips/Packages.xz cfdd9ddbaed505370e4b7054ada267efee4b38acb2eede584efc776ca12ef90e 103 main/debian-installer/binary-mips/Release 3588c6735e797fb410181a416097ba26ce3c2b0675f3037affb9ab93da0e12f1 360842 main/debian-installer/binary-mips64el/Packages 981fefa3728b93c4e3eda7521abac6989aad11c9090735bcece2a46de503e986 78622 main/debian-installer/binary-mips64el/Packages.gz fff6dd8faa5927586778322723ec384934c13a667e5f45f3cf1065b707747b20 66228 main/debian-installer/binary-mips64el/Packages.xz a3135889946204b9cdb22c8f2f435e566f1fd941606a6666b0297adc3a8baae4 107 main/debian-installer/binary-mips64el/Release e96f583adc4d9cae787b291996c74b740db483e5871ff8504e8fba9f4a975d87 479718 main/debian-installer/binary-mipsel/Packages 8cecff99689c71462f9fa5aed2e97a69ad62a692cfb607691fc853569ae5a8a2 94984 main/debian-installer/binary-mipsel/Packages.gz dd256973f6444cb4089638b8c5ab025c7ce404ddbb7029c7eadb3057b25d0231 80008 main/debian-installer/binary-mipsel/Packages.xz a4c0b9ba49c406bfdb0d564e7daaf8b2d4e0cf978ca37b2981fc970d68557dcb 105 main/debian-installer/binary-mipsel/Release a1b098e7265ddd02ead590a9bfbbc917b7605a0bb396095f389e4a08e99bec3c 310055 main/debian-installer/binary-powerpc/Packages ff2364ff0722a2948e83f377fabcd8561a39943c268e44f908d0e85bbf8a043d 72594 main/debian-installer/binary-powerpc/Packages.gz 56a27b1cab18acf6c50b1df79f299a2baba2ef7732c5fe290839b651096188e9 60748 main/debian-installer/binary-powerpc/Packages.xz e58934ec639194e73fefb1d44b0bf5866adce147652d74126c5f401691241e6d 106 main/debian-installer/binary-powerpc/Release 846c39a7755ffd13d3094e4de6ede4d5e06ff5ceb46a98ebd28eb53a1a5ffc12 241368 main/debian-installer/binary-ppc64el/Packages ba016a2b69af3eb52bed2e210009916e184ed2498cc2a1ea4377563dedad380d 61537 main/debian-installer/binary-ppc64el/Packages.gz 04480266634949d89e97fbc59afdb07962e8be21ca14e115c1926a556048c1f9 51868 main/debian-installer/binary-ppc64el/Packages.xz 29eaee9b665539956ef860f083f3885b3d69145a8c9bd33dba701039a2c46e00 106 main/debian-installer/binary-ppc64el/Release fbec413dc2c1fbe272e53fba5517a6d5075119217c938a5f4af5ce223e030421 217996 main/debian-installer/binary-s390x/Packages 2d636884fb244098b0d73a122feeaa340ab1ab05115e45e0b3c69c3ced77b667 58135 main/debian-installer/binary-s390x/Packages.gz 3c0555582da6a91b1751c5f5e631d5f07fc31d981c7034fcef0be6d259900439 48892 main/debian-installer/binary-s390x/Packages.xz d9226f019e4de7c67111161156d98d7bff3eb72164e69532c0e9066b2a625f7c 104 main/debian-installer/binary-s390x/Release 2b6add80d68dc7c65a6f404e2fa5ea35bd2b418b332c070ffacd554e73a9416b 7796441 main/dep11/Components-amd64.yml 2867e0d90c64568e2a25c371546c9c04a01dc14f549620525c9c3e9d1870b8bb 2854073 main/dep11/Components-amd64.yml.gz fa51b492af02b7540e084b9b363a0635e6387a54f6f4f9fe45992de0295dc6ab 1850876 main/dep11/Components-amd64.yml.xz a668a7255f4a68a8026dbe8c33dc662bf65fd7da79d44cf7e1517e34d5322293 7735559 main/dep11/Components-arm64.yml 744e29258913f71ec0cbc2090f6f0eeb819c7d19958778d7c585c23cb3002366 2835371 main/dep11/Components-arm64.yml.gz 19c48720dfda940ece0476a634f35df9372d69a590ee7675a39058aaa34cb6fe 1839368 main/dep11/Components-arm64.yml.xz 6920470671875e0f9a18363b95eef8863afba0d33497edfe0a3a9fb17d6f6550 7744621 main/dep11/Components-armel.yml 28ebd252d9e9b875e60dbe30a542111decd4fb6c043938d75f32bc7c3b802d08 2837621 main/dep11/Components-armel.yml.gz 04d403c5cc79c1333bc3db39b2becbe786d4257a25fdb3a795de2e92baca4ec8 1840304 main/dep11/Components-armel.yml.xz 6bf47cb36757a7b19972cd7151fc2b4c55c2eb077d30fc19103c08410184f19c 7753412 main/dep11/Components-armhf.yml 7aa5a9da1c7ea0a0009d83052bd37bc0f6c8741f4a83e9f956a71d8e8f4d1a04 2838544 main/dep11/Components-armhf.yml.gz c75953362cfc75ab475bbef98e717ad653380065bc1a7b015f39e5fd6ccdf6e1 1842132 main/dep11/Components-armhf.yml.xz 48ba32b1c10b41f4b28bad40de687da7fba63ebb782d580b656509d30aa3b969 7795011 main/dep11/Components-i386.yml ac60bd291efdc7cebb782189c0331586362b15f118c3f36662fa3b79d46354f5 2853321 main/dep11/Components-i386.yml.gz e565ee62f2ebb7a23e7f7de3d28332d254ef910f21b92ec8523ac0f498d016f8 1850760 main/dep11/Components-i386.yml.xz af46dc64347af15c1a48d605fd5d05438862ad606e5bf37c4cb594499787eb2d 7285280 main/dep11/Components-kfreebsd-amd64.yml eb85a70159fb91a20a5a102b644d8b08d24178b969eef1f5be4f688565ff530f 2669347 main/dep11/Components-kfreebsd-amd64.yml.gz 986203b8bca15bb92fbc61654cc7d0095f5a120a8210768dbc824061b98dbe0a 1732772 main/dep11/Components-kfreebsd-amd64.yml.xz 48aa792031f676e2948dbdd75619b5f445579b6fa4db1585f18bcfa55ce3a869 7747672 main/dep11/Components-powerpc.yml c97308a0897ca0035768fb7dd7c0ee2ef8982cc319572b6805524ff11ae0ad04 2838839 main/dep11/Components-powerpc.yml.gz a56885af1975a131836d7284158641efd0db99244080b31628e67a877d4342de 1841208 main/dep11/Components-powerpc.yml.xz ed7ab3be19dd7946d75807813d3d809fd597d7330da6229b92a8559c82d4e564 17068032 main/dep11/icons-128x128.tar 3926d53c6478a1144dae0c05fcf2655850f35cc10b17403a625021aa38a86448 15132129 main/dep11/icons-128x128.tar.gz 1284747ddbfad7092ee234d0f605d57ebe41c96b63e152f9da469d8ec738a5a5 8170496 main/dep11/icons-64x64.tar b39ed190c7e14931a1b5087d27b6caadeef5df7633691ef74920d13c45b54e65 6466066 main/dep11/icons-64x64.tar.gz 1e374870b76e80b982d14e26dfa0f8c6ee2aad8e39e50721cb0f9f8ad28a8a8d 8628 main/i18n/Translation-ca cd2cac5de40e25a1db1d4bbf9e43ce8a29923844b12383aa985b8db2cfbd7774 3543 main/i18n/Translation-ca.bz2 aa8e6e1d8e47de43b9469a8a3eff30a33559a20548949b40c352b420ecaade8c 1755527 main/i18n/Translation-cs 3d066b1c89a7ae5310f6e63824e58abc4b02988f11af134128d81e7be078ab29 456958 main/i18n/Translation-cs.bz2 2f278e7384dc57159e39fadbac82d94604b1a3ab8985c4da4d8cb3d2681c42df 14512 main/i18n/Translation-cs.diff/Index 3072be9daac7708d563f054e71759ab8653a793cb1b0e019548f29a0c5cf7ddf 17190796 main/i18n/Translation-da a07ae07160c3d3901b3866eb5dd02529ba9a03e225410e23b928d3e36c482915 3716728 main/i18n/Translation-da.bz2 918c8bb270ee2f667eb8c0e62aca9bef4fffe6afd6a40f959060219f7c134511 27910 main/i18n/Translation-da.diff/Index ceaec3203741359390d48d864486fab638196da908266e9e64558fc8b0c59ab9 7563488 main/i18n/Translation-de f117b1a1c2b3c7ee5dee0e5e618efb05edf354931966554bd38244312201fbea 1724541 main/i18n/Translation-de.bz2 9efc4bbcf325f3ca872b72bbb664bd682ffc7c7503f7ee42d2a5f7da34872dec 27304 main/i18n/Translation-de.diff/Index 284169348b8bd4e0de4cc5641eeb05577e80d2bd736452e454976c052cf3cbe2 1347 main/i18n/Translation-de_DE 481a435ad350105b74c4972859c44f447b7a8b5edea0d42f6dd635792e00a461 830 main/i18n/Translation-de_DE.bz2 9f9abe444c71a7c8c9541f626211c000e4f311c8ea0a9061ff125c53ce18a210 8287 main/i18n/Translation-el 8e4906ba999712818c7839947cfcd30d3575e7e6462d7c3c2a230f9ce84defc0 1896 main/i18n/Translation-el.bz2 973cac2647ea8e439c6f7b2b474d1d7b6c0847df2f261e96546a4f88337843db 27076273 main/i18n/Translation-en b04cd0855aaf3f70d76ce378b1c508a3d5945f4e630e78c5f4ce4c32408127d9 5427424 main/i18n/Translation-en.bz2 e422327bd3382f40cc0b29283106fb1d3eaf17df7b59dac808f63a654aef85db 27910 main/i18n/Translation-en.diff/Index 847690699f812c7d0e3f424aea63dbe176a0a5d4038a02e9aae5f27850b2ec81 2517 main/i18n/Translation-eo ac072915bffd0f6a205f28dea821a070c349af7bf4b65b4179aa0a915bbbf08b 1279 main/i18n/Translation-eo.bz2 4095465f33a6bd62ad5d5cc9c4f85b12fdf6530aa37dde9f2a974bc2258b8a1e 1326503 main/i18n/Translation-es 45a1921e999a7757933d302d99439ebf5a89f1027ab6cbf6fa1b6a36ff68093e 323379 main/i18n/Translation-es.bz2 3d5c2442fc311560e27bab4a69d8827573ef7864f9b6e038fdab7cb229918b48 17464 main/i18n/Translation-es.diff/Index 50f69276ee42d67f783d4ed8ab93fde3af4cd1b46cf826fbf39cc77b38ad919f 14383 main/i18n/Translation-eu 912db71591f0bfbddbb59bce6d757dd27d72ea3f56eb8e2397cb6c41607e2d72 5475 main/i18n/Translation-eu.bz2 18f79c4119f514ce2ed2a4af62bb547d249881125869202dc8454693e30c8c1e 736 main/i18n/Translation-eu.diff/Index 0bac49d589ae73bfeb86d62d3d633f2ee29b7d689828512781df171b86f970ab 386506 main/i18n/Translation-fi 7ba714ae1b6a2a0ac7c76727d01d598c0f7f3208ffb558e7cb2a7d92ce2152f6 106618 main/i18n/Translation-fi.bz2 c88bc10f4d96bb82cdc9f48940e177f3bffc81af2f0229a557f5424556a838a7 11068 main/i18n/Translation-fi.diff/Index ac779bed66162a7bab1d95c119a7e849043f9b38bffe19ac30f92c8b8e2ce16a 7401641 main/i18n/Translation-fr 948c03e5e34aa7ce88caeadc5874b9bc76666d4cb88d30955a802afe287be488 1518856 main/i18n/Translation-fr.bz2 a3ebcdc75aaed293be328248934f9d26797e7171768c6a1b1d07a227d9992fe6 27796 main/i18n/Translation-fr.diff/Index 5ab03ce3f511cbb5a361ea64ce89e7ba49e01f651fa2e1d4af3a0b10021c3689 21899 main/i18n/Translation-hr 033b8b7b0246dc1d6c75df3abcb236960df570f8c04b7bf3a29e748a3de0434c 5692 main/i18n/Translation-hr.bz2 ab2f5608641965715b677ac5fbc57bc42031154632b02954947f9aed996821bc 2212 main/i18n/Translation-hr.diff/Index 89bfe0cad2885305d5d1af0c78a1b7f18994185b8f60480c0107d81ce1f34438 96810 main/i18n/Translation-hu c3716593bdb1f60488da35fe48e786423e02a24f782aae023e65fbd3154112b7 31851 main/i18n/Translation-hu.bz2 c8bafbe9e089ff8a94b0e86da15b485ac71235a068fd9ba628d89916b8348da9 1720 main/i18n/Translation-hu.diff/Index 0aaf5db67afdf995c5a728194e9715d26a2044ebd50cc5fa022c2371a4d85513 7124 main/i18n/Translation-id 6fcfabedeec60499786b609af2e7154c20307931e976cd2cda4bbf909ea1c6f8 2899 main/i18n/Translation-id.bz2 46d5a128baa65a889e5cb5a43fdb837fa612cc1240c381b57b1deb578aae7e68 21789753 main/i18n/Translation-it b01eb20d0522d2d1e23c493af214975ea598d9ad4d08a019d0715e2b31dcc822 4268806 main/i18n/Translation-it.bz2 c7c1db21789f0d522822accc7759a421cfee797db9ff8cbc92514ed5b85d8079 27910 main/i18n/Translation-it.diff/Index d774a6cf0f8d3506b605cfb21737bdb7fb8d03aa5defde3ea8a0f91f5eeecfd0 5075815 main/i18n/Translation-ja e4ffc9dbe6437620f8f2311920cd8f6b1a2e00d1aaca64ed4437d7ef12e090c6 913629 main/i18n/Translation-ja.bz2 650a491c720acf9cce36a88819e589e9900b550804c592de03b7e4ec75e08ab4 27304 main/i18n/Translation-ja.diff/Index c4f1d19666e0750e947499a33d16ca76049d21d460e309ef232172b336217c14 19077 main/i18n/Translation-km 69632d98e66c1a7056d963ca6a018984ddc28d430c04988900198c48396aa73a 3407 main/i18n/Translation-km.bz2 5960f70ed862adfc8184ac6717bdd0c309fb88dfc2f515624c7c55379baf9fb9 1720 main/i18n/Translation-km.diff/Index b6ef66cf3a04d86c92f8a626603c49a1b8bbbc0510947f397c6e61a28dccc0f6 1631004 main/i18n/Translation-ko 5b29de7d451e521a6d34e58f9cf1260fc911036dc899df721ecc08ed69b141d2 348522 main/i18n/Translation-ko.bz2 da026ac56e180d0d6a5cc209f973c4f79307d7c600d1e25be818ebe61c6accfb 22876 main/i18n/Translation-ko.diff/Index e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/i18n/Translation-ml d3dda84eb03b9738d118eb2be78e246106900493c0ae07819ad60815134a8058 14 main/i18n/Translation-ml.bz2 917a62d4d2bd8fb889d77caae2e4ff39abb23410919ecdc553e8aa4f1854da36 2325 main/i18n/Translation-nb 487379711cdb467cfdc7ff412a0a844796bc8371252b051fc1b325f9ac90832d 1302 main/i18n/Translation-nb.bz2 55cd046521e8af3db8dc99c8ed1efaa7d90ebf901c2a507eb0fe59d3a7cf815f 264671 main/i18n/Translation-nl b0b66f8e47c921744974f857f50710f749872a89e60c7f5be62dcbcc255b6ebe 71487 main/i18n/Translation-nl.bz2 1e2c3990e5a3e7bd37bd6e0d14acd1cbd967e4823efb8cbea827283c68d5d95e 9100 main/i18n/Translation-nl.diff/Index 3d3304429734bb7fc3a6c2d2ae96c18fab0915085f91029526101d4e37a7c7ce 2458040 main/i18n/Translation-pl 9f70c5b1bb9fa6dd8134fc38347e4fcf87397974fd1fa04ba30cfc4934554954 569778 main/i18n/Translation-pl.bz2 1e65e5ec83def88ece96e0874fd7e4804786bc865ea350c1cd5c00871aae2e3f 26812 main/i18n/Translation-pl.diff/Index 5be72155cee0b61542ef0adef00ccee2cb5c00094f5651f9e56971586aedd27c 1564381 main/i18n/Translation-pt 8780f320a4ee72a1b3e39026ac64de1568be9999afbb796e72fe513f0ade6a81 386197 main/i18n/Translation-pt.bz2 a1d7fbb28d9e029a7d57db8f8f86b5b9aa826f4ff63cba6ae74b2333cf13a02f 15988 main/i18n/Translation-pt.diff/Index 9d6de6b8b961fe637efee092cf10ca8d92afe0b3571b037fa48689d690451a8c 3548094 main/i18n/Translation-pt_BR aa3dcdadc481f7151650509242ee91aa57d8d8197fed2a30c8e8723d9e5774ef 846754 main/i18n/Translation-pt_BR.bz2 86afd411cd5d8f3aa32e4cecdc7cdc40b8ec23070f0f772ee37d491d7c54b2ec 26812 main/i18n/Translation-pt_BR.diff/Index e7ab43bbc80d3424f08ec3e14ec6219b7bbf86a201b3bf719393a578ab064ea9 2710 main/i18n/Translation-ro cbc7eed8d01c12cb9bb5b551548780739136155c80f26a09b26ec2ced71a387f 1374 main/i18n/Translation-ro.bz2 2c7e2ecb4b65ac45271c34fac0f83eb1c0da0c5393ea202d54e548a7bc37828d 2655478 main/i18n/Translation-ru a19a3c21f9de6314f40158d9b073b0a18ad3c31c30e6e8fc605f14c588314001 431543 main/i18n/Translation-ru.bz2 50ccac5b40920038d458dda1344d5dcc1889471768ae04deeac0d9c5d85f7e38 21400 main/i18n/Translation-ru.diff/Index 8c84a061195c975076d6eac43cbddae7534f6a62b0347cd7c73b504117e02072 3819007 main/i18n/Translation-sk 47a5c04cd83b4a89abcdfdb6eb3b76c934c6a7316959f1b8cbc7b460ee65344a 843783 main/i18n/Translation-sk.bz2 d25e6d70fa015847a12e46074f4b3977a85f8d179062677010c166868284ef35 27796 main/i18n/Translation-sk.diff/Index 5b666d5bc1f0f15eb08f62db37b2f53094aaf566f428b8ada5dd82ee51b05a7b 441964 main/i18n/Translation-sr d6d2bc3af088d4e71653201d6aa27a519775151c3f4faa24f222115b8461370a 78350 main/i18n/Translation-sr.bz2 0179ee9a35260e64b728ad2a09fa20896873ab9bcdadec86b5afb49a905d3347 8608 main/i18n/Translation-sr.diff/Index e4ec2a2b1d90acddb652afe05eb48a53f0661d7238d5e28e2f4fae32c87c4dd8 117165 main/i18n/Translation-sv a5d616f8d207af0da3891952bbee93079daf1f1b3c447724959724000501618b 36066 main/i18n/Translation-sv.bz2 92d7c8c8e4acfcca816e1c1272b17540d051f51a5d42c4219e31cabaa01621e1 3196 main/i18n/Translation-sv.diff/Index 926b4a85b107381c03f2ae77007d2a9e628b1da209a654ad190d720adfbdd546 902 main/i18n/Translation-tr b53b66bec94d548702370107afb1cec937abbd7b5b29ed47e5f64c1f7416414d 530 main/i18n/Translation-tr.bz2 409e387d4dc4cc0bc2f9d74e5d2bef262521425a3f2e4a07e549e92c1bdda5d9 4277439 main/i18n/Translation-uk c90f7bdb85fc0ad529b8865e71fa1da6118ea6eef51622132a90dc51ba6e8c24 669553 main/i18n/Translation-uk.bz2 346220e4eafd56d14d3b4485fd16d050d3799628425a00c97e67c97f93eaec44 22384 main/i18n/Translation-uk.diff/Index d7d16a522f46156ee04a92b716ba33dbb79c95bba4f349cad33ca2766c938b5a 30443 main/i18n/Translation-vi c9cd3fbdc76e5a9ea0b8d77cc54e952041301240158bab996c3fb66b4ae23b86 8837 main/i18n/Translation-vi.bz2 7d2c34fba22297c19cf1b9d7787ae5ff29afa74285df315d6b7f7a19c92f6ff0 2212 main/i18n/Translation-vi.diff/Index ba5bf385ecf4799de85e8ab873c233fe0478bbdd30611baf528aea51ae2b97d2 2799 main/i18n/Translation-zh ebb2e1910c3096f393f46f4fb6b1316e571d805d654c2d97d0ea92ddb2229424 1526 main/i18n/Translation-zh.bz2 300fa6cb66272ff4249883cd05c5435b9a72bb741a30bb3c12e21f4122b08844 334563 main/i18n/Translation-zh_CN a186649a78aa62e308065744c3c1a26ee237eaa7ae92357aed5786dbbd6ff1f3 92636 main/i18n/Translation-zh_CN.bz2 f88dde02c66fa5c190e86d2a8281c5978405ed8de1615baa00d0fa0ea0890577 7132 main/i18n/Translation-zh_CN.diff/Index 52f67312742d998a09bb592592ccf88ef07fbf75c9fef0b4aca794dc8a59249b 57122 main/i18n/Translation-zh_TW 768d5096de38af7c27a732e3d2bb562c3fb510ea4069fcfea01d92698eeee378 20289 main/i18n/Translation-zh_TW.bz2 3693b3534f95bc8809d67a670cdd0d6a6efd1319de1cb1b14215a20807970940 3196 main/i18n/Translation-zh_TW.diff/Index 782fc37c049313ea581c76d8a06d10d894532edd954b2ca85bd0197b3c30dad1 53815 main/installer-amd64/20150422/images/MD5SUMS 17abd745a3e8d79d4bc22dd9f5bded89b1841b7b991eaaf4a105bcfcc835294d 72131 main/installer-amd64/20150422/images/SHA256SUMS e6cfbb7c170fdb53bffe819a90dc0fde4dc2728b37448aac3d3beb595dfc8cb5 53815 main/installer-amd64/20150718/images/MD5SUMS dcfdf871d5d2eb5802c7a9dd2a6dfa09988be610970117c0bb807e4aad0e2478 72131 main/installer-amd64/20150718/images/SHA256SUMS 2dad7cc647954c125cb01aefe3ea660864b1ddc0bd02a3cca240bd7cec3f827b 53815 main/installer-amd64/20150813/images/MD5SUMS 8abab8a8180c02ca3225cc5cdafade4536555759b2b349bd04be1343e9b9a572 72131 main/installer-amd64/20150813/images/SHA256SUMS 09d6149410e83610406431483b528548a58ffe55eef35e416aa2d6c645eb48f5 53635 main/installer-amd64/20150828/images/MD5SUMS 15bebe8a9eadbad640a1fe31f92132b1410e982cf36710ff5b4c4788105f404a 71887 main/installer-amd64/20150828/images/SHA256SUMS 00fba993c2b9a0495fe010cd6519960c271d7a21b424b4b57677bf23b3173719 53635 main/installer-amd64/20150911/images/MD5SUMS 6564ab007ba57e368cf4e27ceb35903eb4e001f7a13fa41ce7fa3c2d21881bec 71887 main/installer-amd64/20150911/images/SHA256SUMS 1982d9bcd3e4df5c4a0ddb8b8a3be4a79cd85ff8379eb78eaf92c0000fd8a643 53727 main/installer-amd64/20151023/images/MD5SUMS 439922d81867c0a1326d2b4918e9e63cf3e083c8eee8e619eafefc1430863c9e 72011 main/installer-amd64/20151023/images/SHA256SUMS 719797fd52641baa145e96b9d68f796d6059e9f5917fb86cebe8ea813d92c6e1 53635 main/installer-amd64/20160101/images/MD5SUMS 9912410703383968f58d0039777da36a822541e5e298b3d62d4027155f04523a 71887 main/installer-amd64/20160101/images/SHA256SUMS 86e5b841fefc66d27c24b28df158d4563adac62d4a03803be9f8176c3c506ba8 53727 main/installer-amd64/20160106/images/MD5SUMS 5a3c5789d8e81f650063aeb81ea01c00c22ab092bde8cadf5cd1e5f57fc76802 72011 main/installer-amd64/20160106/images/SHA256SUMS 2ac430162d2a2633671508abf149f28b1a0dbe122aff8bd1f167e18f2d63a8cb 53727 main/installer-amd64/20160516+b1/images/MD5SUMS 2cca615ea20b9a6422585c9d4954285b41f33b095570ab7fdde096d767059113 72011 main/installer-amd64/20160516+b1/images/SHA256SUMS 23d12f98b74561addd40c415c2a4b63e55eb9ec2d1966c3a544844f5522f1f3a 53727 main/installer-amd64/20160516/images/MD5SUMS d4fee70a41229548ffb8a521bdec7a9427de2f1747d575a434590ee6eada0830 72011 main/installer-amd64/20160516/images/SHA256SUMS 9ba4360e882bd35893b6051cc04da1ca941f599e2c75f350ee22f0c6243d66ca 53727 main/installer-amd64/20160630/images/MD5SUMS 84019019358117e95eb8506e7926f133f3fab4a5b6c171dace6120ac903700dc 72011 main/installer-amd64/20160630/images/SHA256SUMS 9ba4360e882bd35893b6051cc04da1ca941f599e2c75f350ee22f0c6243d66ca 53727 main/installer-amd64/current/images/MD5SUMS 84019019358117e95eb8506e7926f133f3fab4a5b6c171dace6120ac903700dc 72011 main/installer-amd64/current/images/SHA256SUMS 47a2f89ca82421b20984d38888d48cd5200dd676ca51a9342476bc62c812379c 19148 main/installer-arm64/20150422/images/MD5SUMS 08af7315fd23120f3e8ad1af11ac2730a455d6d3b6ed673f427ee003068adf34 25912 main/installer-arm64/20150422/images/SHA256SUMS 4abd1bda3c9a9e8f978f78624e12af33abfe4a63cdccbd5ff30a9f1776fae9ce 19221 main/installer-arm64/20150718/images/MD5SUMS 703133bbbcc9cffdb51279826bd8e9fc3a7f287e44ab95c4587d2b264ee6c905 26017 main/installer-arm64/20150718/images/SHA256SUMS 01d66d6968bdabdeedd0e81c61c0fc812c9e2ac7c216872b69f264c928c75b5e 19221 main/installer-arm64/20150813/images/MD5SUMS ddfe031101b249b46717cb1ddefc90ca1894d9940241aa001b0a62bafa984977 26017 main/installer-arm64/20150813/images/SHA256SUMS c7da876058755ab6c76e702ff0411fed9c463458986511d732d8013efb98fced 19221 main/installer-arm64/20150828/images/MD5SUMS 04aba7cb700da431614fc3cd2f6b0d9c0178c41012824a2ee868055eb3062438 26017 main/installer-arm64/20150828/images/SHA256SUMS 4854c9a6530782e68e11b1ad5feadb5a021342573aea8ca44b73f676d7f167d3 19221 main/installer-arm64/20150911/images/MD5SUMS 5ff60a0ee800fc22cfdf3d24d0776c8eb65a603ea3d2a91cf7626779218924e3 26017 main/installer-arm64/20150911/images/SHA256SUMS 28d6fb70d3135d473ad1910d1e1862b0c651cba341a44555e8517580a80163a5 19366 main/installer-arm64/20151023/images/MD5SUMS 91da9120ed291b45eee2ed227e1714050037296517a3bc7f0f5848ff2a495b41 26226 main/installer-arm64/20151023/images/SHA256SUMS d05fa6e633f69d6e5e67486c86af96c6637816a9f03449c45f231ad408d9c497 19366 main/installer-arm64/20160101/images/MD5SUMS 0ee86a9f4304295a463499cb81ad326b7a308fe3f15ed567db5455afa746fd74 26226 main/installer-arm64/20160101/images/SHA256SUMS 40219645a6c166f14761286fefebeeeeef077dfeccc375f6d703d4b9810b753e 19366 main/installer-arm64/20160106/images/MD5SUMS dada57574e0389c9a2688ecae6bdd5631d1afea4962415af9f774abd1e15a71f 26226 main/installer-arm64/20160106/images/SHA256SUMS b77a665b60cc6c563629839821b36d1d74f11c8bca932b2de2297bbbe986f601 19788 main/installer-arm64/20160516+b1/images/MD5SUMS a8dfaf96ea5fa2d086bfb2ad1308feb1a69dc99e440748c73872d1489d237abb 26840 main/installer-arm64/20160516+b1/images/SHA256SUMS 7543ef2b46b7339c2c2d654fe80753d71c67be4303cebc6068cbeecee6f0134b 19788 main/installer-arm64/20160516/images/MD5SUMS 60d321071941462dd9e2155a6b6754dd035c7c411d830f6d6181a2809c8af693 26840 main/installer-arm64/20160516/images/SHA256SUMS 19d318ff132aa6098e0eb9786a445565e64c425fd69bb1999363dd7cd9c99838 20520 main/installer-arm64/20160630/images/MD5SUMS 9ba172874b238fe47b6ced472f43a36d69727ae0acb6408c17da79f9abc03734 27892 main/installer-arm64/20160630/images/SHA256SUMS 19d318ff132aa6098e0eb9786a445565e64c425fd69bb1999363dd7cd9c99838 20520 main/installer-arm64/current/images/MD5SUMS 9ba172874b238fe47b6ced472f43a36d69727ae0acb6408c17da79f9abc03734 27892 main/installer-arm64/current/images/SHA256SUMS fdbf3f1cd1be6e4192103e2af7e0d7df528af5c3786fd04f5ccf4ed9dc47529a 8985 main/installer-armel/20150422/images/MD5SUMS 8a5e9f96a5574c8082ff1949b77a49b97593274fb4ee9d188609b2c849d534c1 12645 main/installer-armel/20150422/images/SHA256SUMS 248531e71eb0a18d89923b0374550811453fa5cef8779fc49cc8b0d86b3563b7 10136 main/installer-armel/20150718/images/MD5SUMS ea5743f8661e7273e111597ea888031b6532c83a6649bd69ed6f165f392a039e 14244 main/installer-armel/20150718/images/SHA256SUMS 2dc35a8a631ecf115d01255353b932b8fd82aa6605683685c46e1ff49ff5840e 10214 main/installer-armel/20150813/images/MD5SUMS 5967ca0fba0bb9558193009f7d4778c13cc3fc47b5a55b0589185b7b55a92e21 14354 main/installer-armel/20150813/images/SHA256SUMS d28c0cbb8fa24ea558f1072bd690b4a3dcbc053ab565aed9bd28aa83a36fbafa 10214 main/installer-armel/20150828/images/MD5SUMS 7d08a5c7d823980aeee48df7ec5e69af33b32da758e71b45d18b4e2bf4a88946 14354 main/installer-armel/20150828/images/SHA256SUMS 1ed3e386fb662287f943c27bedf0befca540507b4654948705e036c48d53b6c6 10214 main/installer-armel/20150911/images/MD5SUMS 8ebf74083a551d294208263250f232ca13f38b13ed9e60fc4234d7b06653dbb5 14354 main/installer-armel/20150911/images/SHA256SUMS 968a647886f71779e844ff141d6ad19d4ff800080842deac1b0cbb1e8688e1be 10214 main/installer-armel/20151023/images/MD5SUMS fb9e14b2d95861331c561b450126182d892322d35a9b1b0bf9cfe53c760693c1 14354 main/installer-armel/20151023/images/SHA256SUMS e732991de54c567276f722af50167bca737f3a6bce69155779109826bb7dd231 11320 main/installer-armel/20160101/images/MD5SUMS 109999ccf0974ea3ecce3b0b4d3100dc9a4617cef6df278ccb52f64198e2d6b5 15908 main/installer-armel/20160101/images/SHA256SUMS eb3941390e4aaafe57e88ecd25081d3995de940fe14988e8ea49c3a769961516 11320 main/installer-armel/20160106/images/MD5SUMS 469bcb1a044fc4b58f19d1e44cfdfea0f3a723f7ee2c8d3ec23ee5177d86ced6 15908 main/installer-armel/20160106/images/SHA256SUMS e25e5c6603f7e3e279957c7fa029ab00a574143a7e85dcc1a15250dd5c3e74bd 21918 main/installer-armel/20160516+b1/images/MD5SUMS 9fea0634b1dbdbc167a16234d9ad980f2675c70a3fb0c69670c2e5a9ee516a2a 30634 main/installer-armel/20160516+b1/images/SHA256SUMS a35016c77831e5757c8f56634ba1cf04eb317fd927f4d9303f509ca9f2aa2799 21918 main/installer-armel/20160516/images/MD5SUMS 8c317f5898b8af5b21d17f33945d629c5a5c9d9a00f7d7620421591afa89f572 30634 main/installer-armel/20160516/images/SHA256SUMS 7876a5c20d292f8b54d42398a85ccec97984f9c84cb159684f641719ce37a57a 22683 main/installer-armel/20160630/images/MD5SUMS e611bef6f697250244aaa2d99f915b287e2316e048a011234723f9e93f5aff4a 31687 main/installer-armel/20160630/images/SHA256SUMS 7876a5c20d292f8b54d42398a85ccec97984f9c84cb159684f641719ce37a57a 22683 main/installer-armel/current/images/MD5SUMS e611bef6f697250244aaa2d99f915b287e2316e048a011234723f9e93f5aff4a 31687 main/installer-armel/current/images/SHA256SUMS 8ed796ad892478a3d68d7d6e5ab0d4f0106a45a48a1bbd0c812474f029fde7b7 19599 main/installer-armhf/20150422/images/MD5SUMS 983d76be48052b2f1bf31cb984ec93f47468da67cbc4284288a0ed2a9a6c93ac 28379 main/installer-armhf/20150422/images/SHA256SUMS d662fd2ec159ea1b4fa3b7f2a87920434c62a5104e17a43723612f028915e615 21631 main/installer-armhf/20150718/images/MD5SUMS 130104d66e55c1da44c0a2ab3f161c72e32d81a59074987585fe67eba733199e 31339 main/installer-armhf/20150718/images/SHA256SUMS abc279bc67b5a501e02b49d224072d429132e1fd662c66d1fbb69838465afb5c 21993 main/installer-armhf/20150813/images/MD5SUMS 9dcc502decab968d2a197b16e0548d98770945b22cc2460a36ea849e821e85b9 31861 main/installer-armhf/20150813/images/SHA256SUMS 24416e54c70cf47a3833465cdc620dcd8c2981c1339fd516731f2cfb6b64e48f 21993 main/installer-armhf/20150828/images/MD5SUMS 28fb5018ad1dbc59e25f853dbc4dff064e709f1a3c61630ff73de8bd669ae05a 31861 main/installer-armhf/20150828/images/SHA256SUMS 634ba7496493b44b315e69f305cb9428101d91cd2e83a3966ea0c7fede87c277 21993 main/installer-armhf/20150911/images/MD5SUMS e739d9e0664935fefbd047ce6269aad77f08fa69442e0582353540dfb831c1e1 31861 main/installer-armhf/20150911/images/SHA256SUMS 1301b0f13d404b76ff764e1feeccafbdfe1e3c90e952374e31db294cd81c8f99 23951 main/installer-armhf/20151023/images/MD5SUMS c691189cea644d6c808ea6ebd40be096879ddea3be429ce1e8a0389f654e13fd 34683 main/installer-armhf/20151023/images/SHA256SUMS a4a46442da62a087bda4af0683aebda23e3f09ba0a0b3ea31bf0393ebc7452c1 25780 main/installer-armhf/20160101/images/MD5SUMS df3eff2db5c56ff2cca5c28f92abcd81d0ea9e934b931df5c72e37b48ee6d1ac 37312 main/installer-armhf/20160101/images/SHA256SUMS ccdb7eef05d624d0b1a35f814e9e6bf0974719fadb9de3207c0b473d0d367aa7 25780 main/installer-armhf/20160106/images/MD5SUMS b6ecd8200600151feb3ff913414b86de89a82e5c5fe16008d71bca4d57c89729 37312 main/installer-armhf/20160106/images/SHA256SUMS 59ebd429a2981cc6b5c2b4c7ac9be9a95c7c49daf4726000f351cc13612512d4 32094 main/installer-armhf/20160516+b1/images/MD5SUMS 73a2e3d2ab212e4fcb99d01bb33b4e124356abb96f668a8c094da544188fd2fd 46346 main/installer-armhf/20160516+b1/images/SHA256SUMS 36b4665344f237a0a8bd23bf02c25bb931b4690b9cbcd030ad933db204d46b99 32094 main/installer-armhf/20160516/images/MD5SUMS a1607ebc68eea6a17ff10ec23101334a6492415e78afa7daae2bbe9719c2ab63 46346 main/installer-armhf/20160516/images/SHA256SUMS 6a16f9fe1409e46adaaf6e0b5826d34e8b0ab923e64e152b5952ac980b645a06 33942 main/installer-armhf/20160630/images/MD5SUMS fb9ff4edbef6f1e57aab7d6440610665d296fdc4a94347cd5d05ad1b8f8cf7bc 49026 main/installer-armhf/20160630/images/SHA256SUMS 6a16f9fe1409e46adaaf6e0b5826d34e8b0ab923e64e152b5952ac980b645a06 33942 main/installer-armhf/current/images/MD5SUMS fb9ff4edbef6f1e57aab7d6440610665d296fdc4a94347cd5d05ad1b8f8cf7bc 49026 main/installer-armhf/current/images/SHA256SUMS 0c1498d49c3cbba310a31b29563273272237c49effb30bbb57a3e618e03bd212 52495 main/installer-i386/20150422/images/MD5SUMS 8ab3f5b9dbf85cd95ca3fdd96e47f5145f9119b2501bdb4774e9da644e74c8e6 70875 main/installer-i386/20150422/images/SHA256SUMS b9e06e30940c32ad209d1a0887f9ed81026c19eef61764a4655ceb385d88f83a 52495 main/installer-i386/20150718/images/MD5SUMS 8222f2d6c49b2d0095989b568f431358ededaeaa40bf5732139a62e4709e0e42 70875 main/installer-i386/20150718/images/SHA256SUMS bf07440ac55d9bfc367c11c5c2a3c8896a3459d9405492c197c80f4c883de294 52495 main/installer-i386/20150813/images/MD5SUMS 1574b953b136ba24ca92ee02217e14d6a8bf44e7be4034dd8abefcfeb1c0b891 70875 main/installer-i386/20150813/images/SHA256SUMS 5ceba692b7179e2ec1d230f54b8d4090f2d9e5bb6240e7ee17f64f3e72bd46e0 52317 main/installer-i386/20150828/images/MD5SUMS 27009193915c55e9f85cc49efcddfcf1c07cd2907105f499affd2555791ea927 70633 main/installer-i386/20150828/images/SHA256SUMS 860b23452d3ae00802e70f601680cf315203b7f9613869b315e74334f8286220 52317 main/installer-i386/20150911/images/MD5SUMS 1a8ed126619384b1cc1b17f4491770f4fad122879048079787cad8ea580a05e1 70633 main/installer-i386/20150911/images/SHA256SUMS a953c5bf53c29404e0bc7cd7f2e160dcc608a22d2b1e6ff88affdf23b269a419 52317 main/installer-i386/20151023/images/MD5SUMS 24f172b036aa1b8e8f7bed471fc26dea0f170bd7845e9cee82f8ad85bb42cf88 70633 main/installer-i386/20151023/images/SHA256SUMS d2cba4e3610c1370f90b98b571d18804bb0d02aa947f376b75a0a6ba48088bc8 52317 main/installer-i386/20160101/images/MD5SUMS 03d1176a02a38acb945a91acbdb1acaa689d67d2fa6068d8fd987138eb6e4cd6 70633 main/installer-i386/20160101/images/SHA256SUMS 984247e818959ef73b492040dbc086a436f0cabe200f1a29082531fe0968f4fd 52317 main/installer-i386/20160106/images/MD5SUMS 186667f8de974f70672f4996fbf2da8a5aa77d25ae513634deeb6c45d7d42637 70633 main/installer-i386/20160106/images/SHA256SUMS 3e764aaeb4af7a232ae9fbdca8494a27db81b1d03f17245e50857dbd6a63f378 52317 main/installer-i386/20160516+b1/images/MD5SUMS 240d44a903ff446d26ed77c5ca80bfc0d85e9e1d4ad7d948f639c9ede532f025 70633 main/installer-i386/20160516+b1/images/SHA256SUMS cefee1ec3c91a3c230033876786d886e6adb2bad1c91c35ca889b9c3e7b8316e 52317 main/installer-i386/20160516/images/MD5SUMS 5143b9694aa02cee9150fb01163407182775a3fb0623c910a6d77ac8380a3653 70633 main/installer-i386/20160516/images/SHA256SUMS 97fc44214ab2e0c1a347e8c8e068fffb7e5f0ba5b3f4aa4dc20f8370df3ecdd2 52317 main/installer-i386/20160630/images/MD5SUMS f0c62829db0a69164a50a4523ce82725956c8fb8092b8afe6d72db92f227204d 70633 main/installer-i386/20160630/images/SHA256SUMS 97fc44214ab2e0c1a347e8c8e068fffb7e5f0ba5b3f4aa4dc20f8370df3ecdd2 52317 main/installer-i386/current/images/MD5SUMS f0c62829db0a69164a50a4523ce82725956c8fb8092b8afe6d72db92f227204d 70633 main/installer-i386/current/images/SHA256SUMS ca6b0d3105ed0e01eaa9c71d869964866af7d1a74e0e550dadc50914a6f0e440 2147 main/installer-kfreebsd-amd64/20150422/images/MD5SUMS a9708f827bd440a9d50e70b67bf62a883028f4e7b5f3fd39d7a1b0bc608cb80b 3183 main/installer-kfreebsd-amd64/20150422/images/SHA256SUMS ca6b0d3105ed0e01eaa9c71d869964866af7d1a74e0e550dadc50914a6f0e440 2147 main/installer-kfreebsd-amd64/current/images/MD5SUMS a9708f827bd440a9d50e70b67bf62a883028f4e7b5f3fd39d7a1b0bc608cb80b 3183 main/installer-kfreebsd-amd64/current/images/SHA256SUMS 09dd35d20e440d715ca8d86f8da943339bdadcb00c0e3d39c6ff01cc0d12b115 1209 main/installer-kfreebsd-i386/20150422/images/MD5SUMS 3944b83d2056f3eabe1280e5dd736b3ed463dfd516fe97fbae88f1738472e787 1861 main/installer-kfreebsd-i386/20150422/images/SHA256SUMS 09dd35d20e440d715ca8d86f8da943339bdadcb00c0e3d39c6ff01cc0d12b115 1209 main/installer-kfreebsd-i386/current/images/MD5SUMS 3944b83d2056f3eabe1280e5dd736b3ed463dfd516fe97fbae88f1738472e787 1861 main/installer-kfreebsd-i386/current/images/SHA256SUMS 0ba9585385f7e4d35f74db8d234e14e0ad97de6c2b1e58f4335b9cb61cbf4d24 940 main/installer-mips/20150422/images/MD5SUMS 637d65df2638cb80ca893ba306af0478db47bd105b8a56a0f2fbf49c8e8b1e1d 1496 main/installer-mips/20150422/images/SHA256SUMS 479cf7017fa0df7b298f3f4d5b0b767be740418344d3c82be760fe06b2852b6e 937 main/installer-mips/20150718/images/MD5SUMS 18c2dde11a8f5b9fcba1b1f31430e3f620aad095822acbdd2b94039fda945856 1493 main/installer-mips/20150718/images/SHA256SUMS 08caa4c6d8ff9ff182df3745341244ec73db6166c5b54b2f7e2ee500781c2c67 937 main/installer-mips/20150813/images/MD5SUMS 244c15ab80d120e5d1a85dfbd6c1d8602ad4347bc0ffb5b6109cdfe052633805 1493 main/installer-mips/20150813/images/SHA256SUMS cb33e93a77e6c679ccb600961c57ec4dd6dd763ef7fedf16832b3239add292bf 413 main/installer-mips/20150828/images/MD5SUMS 9eca0151d107d37fd2de77bcdcb9e110846d34bf0d6213074ec4da1eb45a7cc9 713 main/installer-mips/20150828/images/SHA256SUMS f399f9fb84ce08f7f18caddcadd4f1400d2bcb163553b80b3ff55d71a5c383b9 413 main/installer-mips/20150911/images/MD5SUMS 096ddfc60e44386285637acf52ce251d5fbb3047423d9fa913db38fea1c3af85 713 main/installer-mips/20150911/images/SHA256SUMS 413aebd444439523c581a25a7154f43d4293e2760921d030749693f23b7d19f4 413 main/installer-mips/20151023/images/MD5SUMS 5582cb89eaca93ecef34ec67bc4c9113c2e71d2c1e561870a3affb59f9a44690 713 main/installer-mips/20151023/images/SHA256SUMS 56a3061ad3d38094cfbd8ab010dc5288b8c2a1e566cd2f8707628723341747d1 413 main/installer-mips/20160101/images/MD5SUMS eb10b5016362e98c97c32861c319ddd1f30c798a9a6376adaa940823efeacafc 713 main/installer-mips/20160101/images/SHA256SUMS b262d6441038dccd17fac968788c435503a0cef2d5a956332b094079f24f9c1a 413 main/installer-mips/20160106/images/MD5SUMS 4b4b0574f5740f643ccd799e8559fb1e85083492a4a6a7f90e8fd6b2cf99541b 713 main/installer-mips/20160106/images/SHA256SUMS 3b38f9cebd18e24de6de48c657dd775d8666a7920429db1ad8177287005574d9 413 main/installer-mips/20160516+b1/images/MD5SUMS 8599f38cae8b1723f00800d685091a85cff66b5ea146f425e62eae80a11447d6 713 main/installer-mips/20160516+b1/images/SHA256SUMS 8dc96ed1d3b977955c44afe016701e843d96d87a075e347ee9b80a054fd80723 413 main/installer-mips/20160516/images/MD5SUMS 52763e668941ee05d0ce51a1d5a5f6dbf17b0da9d4a583916a7c8aa988af1226 713 main/installer-mips/20160516/images/SHA256SUMS f91ba0adcc3852b90560336e6a189b39804c137b530ce381d0876037a732a42b 413 main/installer-mips/20160630/images/MD5SUMS 9abd37590aac598ad7baf857a0bdf8fd6313a3e9211531ffaf2bec4e818a58b8 713 main/installer-mips/20160630/images/SHA256SUMS f91ba0adcc3852b90560336e6a189b39804c137b530ce381d0876037a732a42b 413 main/installer-mips/current/images/MD5SUMS 9abd37590aac598ad7baf857a0bdf8fd6313a3e9211531ffaf2bec4e818a58b8 713 main/installer-mips/current/images/SHA256SUMS d6c364dc4ef14cc10ff0db0f30665f597417574549444c2bda4ddde8e6ba49ca 1213 main/installer-mipsel/20150422/images/MD5SUMS 07340180253b71ca03bd55ae026fdc5959741054f030a11f5646d06429211bf1 1865 main/installer-mipsel/20150422/images/SHA256SUMS 715ba5f08d71ec0b1ace75b00b8b8407649305ffaf1921ed76687b993766ed72 1208 main/installer-mipsel/20150718/images/MD5SUMS 7d28e99e99b93fdee802df82d63e78b21cca67961b3c16d9c3eda65c7e993e70 1860 main/installer-mipsel/20150718/images/SHA256SUMS 99bce7bd7b99c5d00ab6824cadd5e32c6df10e5a427bb9112b135989cc565710 1208 main/installer-mipsel/20150813/images/MD5SUMS b31e9342f01944b283d58331210f3eb88c25ff49ec5a29bfaec5ce71b9a5b187 1860 main/installer-mipsel/20150813/images/SHA256SUMS 13fb3fd0b55747d66ec2bd61ba74e2d662e8fb2edd52a56b984092467fd1649f 919 main/installer-mipsel/20150828/images/MD5SUMS 3545a766cb1aefeaa0d435d0cdd5fa9af1cc23aeb5a395f4e55888c080001c57 1443 main/installer-mipsel/20150828/images/SHA256SUMS 924c013e1d964352c4eb1d12cfd81e01908cae55ae073d4aa50eccddf2e8d654 919 main/installer-mipsel/20150911/images/MD5SUMS 7f1c0ffb58fa56aa5f2b258a3e0f21aac04fa8ae71bb860f5a528896faa44adb 1443 main/installer-mipsel/20150911/images/SHA256SUMS 32ed73342a1326d815ccfb4e0f473c49cd335270e367aeedf6ac7bd39776217d 919 main/installer-mipsel/20151023/images/MD5SUMS fcb192cb44f88b090cf57f6e71a32a8507a9bce8e78dd38121f1b38d821833f6 1443 main/installer-mipsel/20151023/images/SHA256SUMS a7887d4f550161290282cd13b44be58a4b913a5d6751bb073b5c5b5ceb01d309 919 main/installer-mipsel/20160101/images/MD5SUMS cec2bf1b643fa79f4350a1c77440bb37111fd322d017bc575ecb82a3fa822303 1443 main/installer-mipsel/20160101/images/SHA256SUMS 8027d271f23fed524c7913d7216330ec2ce8751bc1e9cdb5186a0f304ad49a2b 919 main/installer-mipsel/20160106/images/MD5SUMS cfe92ddc67c87c25fe72be025c4d7ebf7cba60ece28cc86d7f6288cfa4abb062 1443 main/installer-mipsel/20160106/images/SHA256SUMS 6ce5f3702a1d270859611405b959ea29eb46e1e6610cfc18f6ba9ad5c0536059 919 main/installer-mipsel/20160516+b1/images/MD5SUMS 089e9d9a497f28fa9338377263d311a661683f3bd914f7aab3be6a86d786dc49 1443 main/installer-mipsel/20160516+b1/images/SHA256SUMS f835b0ba7e61444a3e21b6129b7c0d4bb54194581860bd46f7a709153e377914 919 main/installer-mipsel/20160516/images/MD5SUMS a5535e9140f0d0eb03a72068afcd14bfd9dbaa5c6fe599649082f105de7b5af1 1443 main/installer-mipsel/20160516/images/SHA256SUMS d7ea52e86fa59cae164b43cd3690e70ebd55d7eddd9d2148719f704d7b5cf7ee 624 main/installer-mipsel/20160630/images/MD5SUMS 05e71fd18cfaa1a23f5da37d9e65c1fd7666dc2856b23a51b8cbed86573b8206 1020 main/installer-mipsel/20160630/images/SHA256SUMS d7ea52e86fa59cae164b43cd3690e70ebd55d7eddd9d2148719f704d7b5cf7ee 624 main/installer-mipsel/current/images/MD5SUMS 05e71fd18cfaa1a23f5da37d9e65c1fd7666dc2856b23a51b8cbed86573b8206 1020 main/installer-mipsel/current/images/SHA256SUMS db19e3daaca03a155831e9b588810291489f0e82be200fab0c4cbd36d3294efa 2128 main/installer-powerpc/20150422/images/MD5SUMS 581181933b42af5df34ae56e335184a8677aa4e4bd6d2f5a6f2566b1e84fdc4a 3292 main/installer-powerpc/20150422/images/SHA256SUMS a67e0d2e4e3c1b112c89a33936bf9b8a3d20e3c6388704986f2eb62dad1f010d 2128 main/installer-powerpc/20150718/images/MD5SUMS 32cdfecc91e1a0dc1f431165f5f7586f29e55ca86f92b178509ad9bef9939213 3292 main/installer-powerpc/20150718/images/SHA256SUMS 03490e0d4bb7ce94852f8625824fec2c5665bf2d02c9c9650ce114a118287aa8 2128 main/installer-powerpc/20150813/images/MD5SUMS cffa2b9f0f2a94155ac678c76ecbb45b29dd629e849052350b0abf696b2c07ab 3292 main/installer-powerpc/20150813/images/SHA256SUMS 1261f0d22f33c517158e013993951a21245d2e4db13af75e996f1a97c6bde862 2128 main/installer-powerpc/20150828/images/MD5SUMS 391b99eac85294f33cc51ad12950ef0096034b1e23660842212fb1b84efd93b9 3292 main/installer-powerpc/20150828/images/SHA256SUMS d7ac606026dcec95d44ebd6e9bbed7362240c89269a197936c2b2cf61893cfc7 2128 main/installer-powerpc/20150911/images/MD5SUMS c876db3c13bc9e5854c95417d33d17e5ac98539359ca903c959481a48bd9672c 3292 main/installer-powerpc/20150911/images/SHA256SUMS 2cec803c37d98d6e183f058299d974b8d60668f215766a6b8318826673327c80 2128 main/installer-powerpc/20151023/images/MD5SUMS d3e29265cacfd970d940b966920b091e8b4931b068bfa18ed442dd0e0d8cf1ba 3292 main/installer-powerpc/20151023/images/SHA256SUMS 8eccc52f6c72f4b20447b6cde843c92f7b040b5c58d45aa29c97898e1ff89943 2128 main/installer-powerpc/20160101/images/MD5SUMS fdb5399c64e383faa6aa6dc3dc2b338ec424c8550419be90e601dbcbbe4e9048 3292 main/installer-powerpc/20160101/images/SHA256SUMS 8dc188bad6d7f37ddfa5d44b356f20b80ff6fff6815a6c5c9ce2d407f79d03e0 2128 main/installer-powerpc/20160106/images/MD5SUMS 080ce2cb26e47953ff05f8ccadd35a2bbdbbbeeaba7343e144b4f2b785098e5c 3292 main/installer-powerpc/20160106/images/SHA256SUMS f7f15e409260256e009cb0e6740b66164c13c0bbfdbed6f4237bc40094ad4833 2128 main/installer-powerpc/20160516+b1/images/MD5SUMS 0c3a8b1f8b5e8793c815a00315bf2e25e122e470322d855f0658fa481b11a180 3292 main/installer-powerpc/20160516+b1/images/SHA256SUMS 3b8996b0e5085dbe4408ca3d31f95b62e52815263a2a1929a0582bc50591d8d4 2128 main/installer-powerpc/20160516/images/MD5SUMS 7b033f34b23c3bf95c247478c5bb0fcc8ef21eca01fa0558cad2932eae6e7d82 3292 main/installer-powerpc/20160516/images/SHA256SUMS ba2e8ce31f4123a4b6b87dba18ac37d559ea1334890e1220111358cff63ebcaa 2128 main/installer-powerpc/20160630/images/MD5SUMS c590b5725804fbcebd681d749722590475810f81941c485439c7d39408e7de23 3292 main/installer-powerpc/20160630/images/SHA256SUMS ba2e8ce31f4123a4b6b87dba18ac37d559ea1334890e1220111358cff63ebcaa 2128 main/installer-powerpc/current/images/MD5SUMS c590b5725804fbcebd681d749722590475810f81941c485439c7d39408e7de23 3292 main/installer-powerpc/current/images/SHA256SUMS 1ddb9a9a243149f93822c8b14e1f1ed2ef69c46c79c2cc2a01c20647346f86c6 576 main/installer-ppc64el/20150422/images/MD5SUMS 914330f644847bb5401fa3160cfe2d79e50c02e3df91bffb1085081a51db23ae 972 main/installer-ppc64el/20150422/images/SHA256SUMS 53efb5fabeba2ce336785cba4b8766744c24b23791222763a4e521a6452782b6 576 main/installer-ppc64el/20150718/images/MD5SUMS 9d9c8c331935fbfd2cd239bfc5ab3faaf7287fa0d7e8b6af6f93ee537c9d085a 972 main/installer-ppc64el/20150718/images/SHA256SUMS ee2cb4908cff721076b5b788b2899a4a6fd08fc726918ee5eacb1789a388af81 576 main/installer-ppc64el/20150813/images/MD5SUMS 721ab0e17a3ba49fc1d4a7ea787353dfb25d6695a5d18456e9638570ec08bafc 972 main/installer-ppc64el/20150813/images/SHA256SUMS 952dc2697724e20b465a9c4ec4518a530cb02153b8cf56f0e14e88dee71b523f 576 main/installer-ppc64el/20150828/images/MD5SUMS 16447ed5c7b09a2c08a7c190c071c9aaa8cb2e20b6eb99cd79aa76a3288d6f68 972 main/installer-ppc64el/20150828/images/SHA256SUMS 569ea20b8badd750fdb9b1aea748db5f2d7a8c7ea75e21714dc69b8e62c3c98a 576 main/installer-ppc64el/20150911/images/MD5SUMS 7cfeffe55a40f3af38916f147cac0c7fe70a301ebd7a7ba3308f8eb86fa7baa6 972 main/installer-ppc64el/20150911/images/SHA256SUMS 50305f8f6a5ee7553e25f596d500c4fc6854d44897392ac00b6c03ee06780f31 576 main/installer-ppc64el/20151023/images/MD5SUMS 8296ee34584c6853b4718d68b1aca930466470613af840f26604876ad57093e2 972 main/installer-ppc64el/20151023/images/SHA256SUMS d0ff8c8255691028757777e5918e49581c2229962a9c74a35205b5720e9e369e 576 main/installer-ppc64el/20160101/images/MD5SUMS 7f59376f89488f4ba2cde49900a3770c3e36b18dce0b49ac76e47a9709d7f336 972 main/installer-ppc64el/20160101/images/SHA256SUMS 73f1744d28d9397dfeba4e644a6d8a5d5d8fd19c0ce2336063f2df4b966ea89e 576 main/installer-ppc64el/20160106/images/MD5SUMS b789bbaf626fa52e84a76d714b2af3cbdfb8fb3b9addd3a8b79d7473b6d86531 972 main/installer-ppc64el/20160106/images/SHA256SUMS 3b247fcbff136c542c34ed117423ff0a9fc4ba2d4925de62feacf13c1852e8ea 576 main/installer-ppc64el/20160516+b1/images/MD5SUMS 56ca9e6483a3e3e45d8521b0f25c114a0b165cd76f99340bf0d52a5d70b24039 972 main/installer-ppc64el/20160516+b1/images/SHA256SUMS 6c536a156954646d5a9b4613f319a2af5d22ad6133c28812f5fcbd711f624fb3 576 main/installer-ppc64el/20160516/images/MD5SUMS c4af2e8671e2a2e973e51cee17d82a7950e22d617d990c096e9db7a4e12a6975 972 main/installer-ppc64el/20160516/images/SHA256SUMS 2d3f61a311bcbfa4d628424fc1743b028e22f53f83d96baea4e083187880ad66 576 main/installer-ppc64el/20160630/images/MD5SUMS 976439ed51ae8ea69470581dd1ef4ee88c4fa305821530786d1be8bcce70a1de 972 main/installer-ppc64el/20160630/images/SHA256SUMS 2d3f61a311bcbfa4d628424fc1743b028e22f53f83d96baea4e083187880ad66 576 main/installer-ppc64el/current/images/MD5SUMS 976439ed51ae8ea69470581dd1ef4ee88c4fa305821530786d1be8bcce70a1de 972 main/installer-ppc64el/current/images/SHA256SUMS 376bfb191054107631fe8de2dcedcefa14b35a6e957a4210fe8b76a79a16471e 374 main/installer-s390x/20150422/images/MD5SUMS f8a4b1aaab7b502945db265c1560a47a641609753b77ed61587d68b3e0fd7218 674 main/installer-s390x/20150422/images/SHA256SUMS f5842aaa240147dd778c8db3b3bf957c76cbf7548bd2149582a11d1209561493 374 main/installer-s390x/20150718/images/MD5SUMS c28d1deedafad179325baaffa0c0f78a24b8a21bc0ea4dfcea01b54b89a0fd00 674 main/installer-s390x/20150718/images/SHA256SUMS 907f84b1f28a76b5fdb470f3e55f0571b3dcda80150d3dba7b74b198b641e1ff 374 main/installer-s390x/20150813/images/MD5SUMS cd0dd8b682a2902dabd5f9877670aa36a4bc054da19d22922cda7ddbd9bd334f 674 main/installer-s390x/20150813/images/SHA256SUMS 5a8f72684269197e6bf762a4358d29376df6f647019a566bf16c033a42320188 374 main/installer-s390x/20150828/images/MD5SUMS 32df3dc845ac26560573ac3aab101431e9951f531c53a6b03919146217067090 674 main/installer-s390x/20150828/images/SHA256SUMS 4a17edc1997b125da90951bbf25b1eacd48a1858864fa6d2de2bfc5b7862dfc6 374 main/installer-s390x/20150911/images/MD5SUMS 0d26154d6cb88e72612b3c5563202829fec6fe5c1812bdfdd12ad86819a35b20 674 main/installer-s390x/20150911/images/SHA256SUMS b490b3d5b51e989886c85cd99be29d09c19b58e754d7626cfd352d3141e988ae 374 main/installer-s390x/20151023/images/MD5SUMS e61867b0825e98dec6f4db39175bb1c76468697964ab430fceb27118bbd87abe 674 main/installer-s390x/20151023/images/SHA256SUMS d162662305450e61311ba4e674967ffdb475b909c5e2ed47fa41b1489cc79a6e 374 main/installer-s390x/20160101/images/MD5SUMS dd2b2f8c0c4fb2fc7fc19868d42da2342d21ae1952c24756f386277a57987677 674 main/installer-s390x/20160101/images/SHA256SUMS 58883dfbdd5b7941c6b4601da6f0fd27dc5f951a78a0782380e1551c08abaa56 374 main/installer-s390x/20160106/images/MD5SUMS f55533695eb1843741aee9dad3a9d9974870e49702929c88b148d692f774a0c4 674 main/installer-s390x/20160106/images/SHA256SUMS e298da017ad8fbfb0e2e8bf59c5fcd8d7ca825bd178eba110f89f5ca6044b92a 374 main/installer-s390x/20160516+b1/images/MD5SUMS cc731f7010dd8d07e572ee172c6cb9fb89a2f95876aae2eedb73e19a5bce1648 674 main/installer-s390x/20160516+b1/images/SHA256SUMS 4a37149f2de23ca21bb4d84c6c12624f24de81d37d57ec72f88600ce162ff861 374 main/installer-s390x/20160516/images/MD5SUMS f941bc95a52be3e8eb44a41c286e8b7dab0d6689f9471bd6336a0589fc5f7ce6 674 main/installer-s390x/20160516/images/SHA256SUMS b74a77a31313079a9e30f4d6c9240d623c417ca9bc28f997cc4e1715f4189ad0 374 main/installer-s390x/20160630/images/MD5SUMS f1bf9df36eec7aeadd4a945385f7961bc7245edc3b4a7926e4e836f16edeb6d6 674 main/installer-s390x/20160630/images/SHA256SUMS b74a77a31313079a9e30f4d6c9240d623c417ca9bc28f997cc4e1715f4189ad0 374 main/installer-s390x/current/images/MD5SUMS f1bf9df36eec7aeadd4a945385f7961bc7245edc3b4a7926e4e836f16edeb6d6 674 main/installer-s390x/current/images/SHA256SUMS 9476f882e92de56c32d5c1057241d2a23013a66384680922d4e65c68fbab3980 105 main/source/Release 8b72060cc35f189f55a1bf7f01d34f2a02f82ab0d21ac2e5dbd1e4e7e79f0207 35489733 main/source/Sources 5595454f6e710e828f13f69e994d5e2569b95c7082f8918f6b83e38b183514e4 27910 main/source/Sources.diff/Index 2ca55dff8b5bcd53c99457c9a6ab7089302025f78638bc5b9d25dbf1b832806f 9072408 main/source/Sources.gz da0be89f252019f12c2772b76f24e1d947b9f5ba8520fbe34ec2a8ddb8ae12be 6901228 main/source/Sources.xz 7b466e0293b45758508a090f33579a16584b0add1ec7fb9d7f553add26cfa3d7 14518404 non-free/Contents-amd64 d1e066e9180d9636424f01cf58f7183d0363a7bbd8c2524a8a005809f808ec3c 27416 non-free/Contents-amd64.diff/Index dfe9b4bf525d3b8c21a0ebc0fa8124b9ba89a21afec947fe60bcd88cedc0aaf1 812504 non-free/Contents-amd64.gz df9868d137f7f7a3e43e0e53bcfef8d9d7a9dcfaa962d0db53ae9407f9e3f92a 13465086 non-free/Contents-arm64 dcdd00f148198438d030a6cffa28b0ea35c125f9d5b5fbb5c683c28eefae79a9 19512 non-free/Contents-arm64.diff/Index 6cee186816f294b289c7f232a96bb4cb096c6467a09ff353d5901c8444efb8b7 731747 non-free/Contents-arm64.gz a6edf4cda478e2538b314b8dcfc6a6b834950fe9b378aba0e4974eb664a3def4 13478582 non-free/Contents-armel db82b93155c372f6d4aeaa641ddbf531a1c1c9d1b3c7da8bfbbc86bce5b42d39 20500 non-free/Contents-armel.diff/Index 4174fdd16f9cd2805b95b5b01fa67e887befc93cebb3914f6cfd74c1597244ad 733675 non-free/Contents-armel.gz 8e04746c8f7c7aa698e7895941d8bc4dcfc979862ee711fd1a17e35a52fa9cb7 13539201 non-free/Contents-armhf a5dc03511b0a5de95e1b2cbd5b6ad9ae9e0d9d172e90139c414157c8e89427ba 22476 non-free/Contents-armhf.diff/Index 827f9ae64d2e67734efa5a3fb8676ba4f3fb44663cc0c1d9ec109bf71f2c0d40 738525 non-free/Contents-armhf.gz 1ff8793a36b1a0db90af6efc38f80bd0758b225d03248eebfee9365cceb5ee0f 13374121 non-free/Contents-hurd-i386 1f136ee18b3ae63a19ac273dd3211b394a07e3ace9be04bd057de4be28d7b1bc 19018 non-free/Contents-hurd-i386.diff/Index e0e69f2ef25fd9c06208c580006d22d85da1090813dfa0805d36dd49434c68ad 724269 non-free/Contents-hurd-i386.gz 3febd7606c2fefad92049e6bc02e765faeaf282c62b9e63d0d7fa2845a5f0dc5 13755254 non-free/Contents-i386 0303a773478fc46a173f687bc8a6147d89b430f7e52e6581fc059b421918735b 23464 non-free/Contents-i386.diff/Index 6adbd1789477a4c9539d8353ae8b38e7764c293873dbae846a2e10ad0fc2774f 757556 non-free/Contents-i386.gz 741fab572d6cf9d1e34852082e8066ba7167b6a0cd98d0500406ebec34d8062c 13514206 non-free/Contents-kfreebsd-amd64 f5247cf283bd746f19283ba79cf8a42ca6f530710bd3e162e51f3286acdc2db4 19018 non-free/Contents-kfreebsd-amd64.diff/Index 9817601dd5d7429bd65c53e28fa5b9163ba17d02285d94d1a3de0c10aac9c024 736421 non-free/Contents-kfreebsd-amd64.gz e79b0a36ba3aad7f395611bf4fbde17e4441ae199b633684ff9e43b76cd9cba1 13515299 non-free/Contents-kfreebsd-i386 793e1143ca9af2a6f7adfca75a65aafab853a095ee0d6ba017f71ccba16c284a 19018 non-free/Contents-kfreebsd-i386.diff/Index 657b0c86081b1be93cc4b451e44da35cc534297aa0647191ee65756e37acd652 736493 non-free/Contents-kfreebsd-i386.gz 0d142d9255a6a6a9b9a3254e2a31050a97625208be9c680e810d84df7934be0f 13468449 non-free/Contents-mips 4084a98e2ab431fe43d73083bd8714c704a674ea2f9066341179fba5d8912801 19512 non-free/Contents-mips.diff/Index 03ce409381d582d0875d3deda19b066abb088bac5bd66cd17bd57210b176ed3c 732565 non-free/Contents-mips.gz 46215efab8e670ec09231851a47b21c30debc9763af9fde2289622583a971e0d 13465548 non-free/Contents-mips64el 50b2e568440f4ddab88577c6f04bd04f092861fc199e94453fccf4e06cc7bbd8 18030 non-free/Contents-mips64el.diff/Index d93ec2ef0dedbfc462dec55408b15ebb551c9e207ef0b84a97536083b128cc62 731477 non-free/Contents-mips64el.gz 73a05e461adcac92170eb10d519733fba7227a113121a5169cbb422ffd5e81d4 13479076 non-free/Contents-mipsel 95933911348a5bea482e3cf29c4bf7d9ddbf544fd10663febe10b9ffef05c7be 19512 non-free/Contents-mipsel.diff/Index bcaca5dc11a0a1f65a11cc878bf2a01249e0db0148d27f107c557363ec64d96f 733446 non-free/Contents-mipsel.gz f478ed38da7ba626a3db6ff70f354985fbbfa5bbc34bda70d1eae1481c9c397e 13467317 non-free/Contents-powerpc cd723abef7ccedb7fc32f97eaadb33c9f1432df55aa248adb50c8644f356b1aa 19512 non-free/Contents-powerpc.diff/Index 071e2b38e011051d2491aea3c99a58ee18ad58cb7b00b56daa39d839483e1124 732439 non-free/Contents-powerpc.gz 574b80334ffd304164db448054ef01cfe68ef960c2ae70c9b4c03b8890c63330 14067773 non-free/Contents-ppc64el e5f45bd5029265a6b2b04bb2185487af47c6f22d44db626d38708c3bbc24192c 19512 non-free/Contents-ppc64el.diff/Index 69c3ea08813c490034200d261814fd33b6864ffb9e2da0deae89b2069d541fdb 774292 non-free/Contents-ppc64el.gz 704ba4ff87dac7a8da5b76b50ff8755f7d480f28758f161d1e6194f5b2b03122 13475399 non-free/Contents-s390x 931eb1d031be7c97d082e89d5a62ba4ae7529a79c7c5819ad69c4d354889810e 20006 non-free/Contents-s390x.diff/Index a6958dffa25b6d2eb985d2aa7282e3bc486ed7f4f33f70728f8c7a1b78b84de5 733090 non-free/Contents-s390x.gz a210ee4eb96e6a1c0920c7c1305c2a04c3123098acef8ab203af955d35be8d85 8203337 non-free/Contents-source 5a61286838c2f021fccd6d2d8e8f6117f5ccf73cf8f11e206d29d94b0cf7fc35 27796 non-free/Contents-source.diff/Index 29daf88e88cc7a443818455ac3432505441681c64eee8fb0079c1e55b7382ce6 878132 non-free/Contents-source.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-amd64 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-amd64.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-arm64 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-arm64.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-armel 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-armel.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-armhf 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-armhf.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-hurd-i386 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-hurd-i386.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-i386 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-i386.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-kfreebsd-amd64 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-kfreebsd-amd64.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-kfreebsd-i386 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-kfreebsd-i386.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-mips 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-mips.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-mips64el 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-mips64el.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-mipsel 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-mipsel.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-powerpc 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-powerpc.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-ppc64el 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-ppc64el.gz 46cd40224a17d73f2128f9165814d5a8050dc3cf3b4a659581adf2148c2404cb 451 non-free/Contents-udeb-s390x 0cff97371dfefacdd754bb45d9e55f0c494158613fc46202ada07a93386a56ff 271 non-free/Contents-udeb-s390x.gz 4100d97c75adeb569fcdfff7aec147bc292062bd168a365703b06bc93f63ecbd 194055 non-free/binary-all/Packages 7d2aecac95ab2512ac91d40d97cb85e22cdb0fe7871b52aceaf459a8b20c74f9 52716 non-free/binary-all/Packages.gz 9dad80df850007b43edace6084987334801b5da517266b05b7c0ebd4177668fa 44872 non-free/binary-all/Packages.xz 51d50056fc0cb0e4394cad60c703bdac7c1a8643df381e7d011347be4b3ba13b 106 non-free/binary-all/Release 31afb60582d59780b4ce929dffe3e5b22533b981952304c97c8129428a485f4b 372384 non-free/binary-amd64/Packages ef49a05ceb3651f1ed050e8b0fa0a5cbe4646e86fd460b62d557493a6631e704 27796 non-free/binary-amd64/Packages.diff/Index 2475be32ba60366fb15d0d679e5bdd5d89211d153f967d8ec23921915be0bfe6 94091 non-free/binary-amd64/Packages.gz b9b76765cb3c02bbf69fb16d9f752419f95d9ede61983933a640df1697811942 78516 non-free/binary-amd64/Packages.xz d60bd5af7b9989c00aa24c2afa89d251f7ff5fc0bf9fa15e47afcea91adcd051 108 non-free/binary-amd64/Release ffddc03eec49bfbc2363a5ced1256c678cae4f4bad6dd515d8cb1eae66db093f 228434 non-free/binary-arm64/Packages f98c6d2889149a5e15080cd98c3e7c74b6d9791292fb6ba988f5d8deda19b58c 27796 non-free/binary-arm64/Packages.diff/Index 86368efe466ebde79711c8a2e002ba724d2727f5c5ae17534dc4bcaa6496343c 62112 non-free/binary-arm64/Packages.gz dec2a87e49d875553356c79c0a6a21ac30f78832afaeca6b2b40661c0f66aec1 52564 non-free/binary-arm64/Packages.xz 946c20f0df385f2d7233b2ce90460bd0894394e8b1bb6b2b00293223813d27b3 108 non-free/binary-arm64/Release 528cde6f11e2bf0572911b4efbe9ee21886d32e89646f8fb82a6da6a536c3da7 235109 non-free/binary-armel/Packages 5102efe50ebe046d3cc463031dff318ab14a3c7b6fde6c9aafef5077eede6f0f 27796 non-free/binary-armel/Packages.diff/Index d045de146a61d117a0aafae0cd404cbfbeb53bf84d90c6be493cfacfc33a8d8a 64174 non-free/binary-armel/Packages.gz c102d5df5a15505d3951895feb43d410bdef34d9394ed02ea4d0ea1c7781a000 54144 non-free/binary-armel/Packages.xz 9984e12504679bfbd364ebfef3ba6406aa0445b71b5b98f52567ca60b23dc656 108 non-free/binary-armel/Release c9a83eceb9f6007530993da64cd2bf00f22db724b3ee76889dd6268b5b8ccd49 275515 non-free/binary-armhf/Packages c28a57071ed9c7696f4e534ded5ae872706a85aec0013d32ab516599a54de91b 27796 non-free/binary-armhf/Packages.diff/Index cbefcdf70d1465762c62fc6aacd3ea3e652349b91fa1fc8e6e07fd4cc788231d 71048 non-free/binary-armhf/Packages.gz f9ddf6b29f02ff4142190106e3999fd756aa995cfedf2f95c6082a18923ccada 59664 non-free/binary-armhf/Packages.xz 4a0d4b3cce6c96e15e0111d7c66a6db69d4bf177a54553b7dc4e024f780bc2f1 108 non-free/binary-armhf/Release 5cd60593a1d15255b9424e3525ba5b08608ec8bd20f17d958ad1c80d40fce683 232894 non-free/binary-hurd-i386/Packages be52bd25199806cc2f88441a8ffff1726b6653c4aea91618ea6fa1d10bf4b203 27796 non-free/binary-hurd-i386/Packages.diff/Index 00860357838ea3b12f64e49125042d0cd1cb324951db980a99c3c8e0ce093626 62661 non-free/binary-hurd-i386/Packages.gz eb79813dc8bef64e642ad912e52d70e8dadd60cc27caf9b261b3eef1fd201dcc 53192 non-free/binary-hurd-i386/Packages.xz 881c495ebeb1c4eb5a95f394574a6114c2bc0f79b8730ca7c94b1a5d152211ac 112 non-free/binary-hurd-i386/Release c6ecce76604875f59e5eccd093540302b2d8365ebed9fed4d4c6f3804a3e29ac 337604 non-free/binary-i386/Packages d28bed36917ca34b84e41a180f438d0335902b370f02d73ab280ca1800b47ebd 27796 non-free/binary-i386/Packages.diff/Index c88d2c07afa95c9bd9ffcd7a037c6941a3686f6ff16980cd7f23e04b09f0167d 85001 non-free/binary-i386/Packages.gz 05e517cab2aa1eb7b475c7042df113ad58f7429eb8046bf3ab0577b21ea233b6 70972 non-free/binary-i386/Packages.xz 8dfb106459eb8e64fb45eb64cc9149575f3ede58182df83611126833f8d0e1ca 107 non-free/binary-i386/Release e782729775b3b87aab318a890433857faf6912531b05eca842e4fc6068023273 238159 non-free/binary-kfreebsd-amd64/Packages 0e5182638df8382789f119c448cba6a90256a7b930acde2d55ff4786714f4d25 27796 non-free/binary-kfreebsd-amd64/Packages.diff/Index fd9d8e7caa7d8ddc86c0c045be22bcb977e2fed06252fcfe5871e85f6553f215 64110 non-free/binary-kfreebsd-amd64/Packages.gz 802fe13e3c4efe2d508fe54a667388a6e253de46bc06065f9cc759a685f71ee7 54172 non-free/binary-kfreebsd-amd64/Packages.xz cf14ba1aa77c9bab4034aa54aa6b65c064aab1e21e83efd216b550e5dfb2f8ba 117 non-free/binary-kfreebsd-amd64/Release 2d101483ae233c09f62f1e81dcf3af439afd3f19e72510e232f0f92017912cb1 238774 non-free/binary-kfreebsd-i386/Packages e3a2c5555f233eaa81cc4b8e445f70a1c4628f9f6a8ece1a306a62e3f19f75de 27796 non-free/binary-kfreebsd-i386/Packages.diff/Index b413bc3cfd68b6a50b494d6ecc46ec68548e649b6a19d63ab022f3ac7dcb0b50 64166 non-free/binary-kfreebsd-i386/Packages.gz 8eff574bdf29ea4d897add8d8cddcad50ab393e8522dd4425c3bd48647be2c4f 54388 non-free/binary-kfreebsd-i386/Packages.xz 6d5ef61e0b5cd01d118249fe98aa27b6bde15b8d7a13c0ce0faf90f75a891270 116 non-free/binary-kfreebsd-i386/Release ce1873dbb986fc660704a90ccf32f9e7246f17b81116bdb8d7af6885d9c7412e 232072 non-free/binary-mips/Packages c55b6b1c99ed805832225c45b2a0f61030d4245b1ad326d0da1da4aa45f8f762 27796 non-free/binary-mips/Packages.diff/Index a531514c26f99ccce30136e76851be3575aef7a1f9b946f1d50c719c9c4cb1a0 62918 non-free/binary-mips/Packages.gz 439e13ad8a83d4d5ca579093d4b282b30d53783635a2e1bc07c421cdb36a26ea 53340 non-free/binary-mips/Packages.xz 143ca71226997dd2873eee4118fe26f3443f036fead6ff149c614e2db5f22202 107 non-free/binary-mips/Release b90d0bbb7c1a0f2d8d4b98be86dcaea60b31fc0cf478cb7da04cfe1c7a4765c5 229428 non-free/binary-mips64el/Packages d8732d7d6877521b04c90616980349bcfaf86a7d9bf687c297ca5083de1cac4d 27796 non-free/binary-mips64el/Packages.diff/Index 90eda650543dc7e647337ab44720c8c49a2ec851b4011d6d57a342eefb34c428 62084 non-free/binary-mips64el/Packages.gz 14d9dfa46ea0f85c0b933b7b21aee9dc40c5e0726692a6f2529c3f2cf609c3ed 52544 non-free/binary-mips64el/Packages.xz 8fbe42254ca8ac1b019a4fda52d912da4b93e95ae0c9eb04c7f4748d0bc50057 111 non-free/binary-mips64el/Release f804875d9c4efc3a5ae31f28aaf73c89eb30802d0949e8cc878482ef9ae7865f 235706 non-free/binary-mipsel/Packages 7386fde81da292b36599adea45e373aa519313f397140fa32a5f7bc641c9e825 27796 non-free/binary-mipsel/Packages.diff/Index f79838a5e4e4d2cf79abebb6c27cd77695373e78015ed1306d8a716c56921772 63921 non-free/binary-mipsel/Packages.gz e35ec9bf1969deb8f81a14b8859cafc090d9ead6e416ad2d088af42acce53171 54036 non-free/binary-mipsel/Packages.xz 32e889dd01e87d5bc03c0ef9bfc68601c2da6e777369be839f6833faf7586c05 109 non-free/binary-mipsel/Release 14a04b696178a51762c32a2e0e17c8b22686c1d334b15141f4255f7ddc42700f 231104 non-free/binary-powerpc/Packages 0443dcea310ea31f428851d281f4664cfc62263056dfc16b110cdbc749e141d0 27796 non-free/binary-powerpc/Packages.diff/Index 00c1c45b5ffab213413f780d808163dfde5573e8b1fd30453043bb23bc2d0dea 62821 non-free/binary-powerpc/Packages.gz 079d8ee2b944ba6a0407627933ef4b049a15e3c03e0bb3f37df816fc8d62db7d 53196 non-free/binary-powerpc/Packages.xz 2f69def9d89a278cfef84fd0e096d9f1ba890c128f9b6a40c9b6b71fe968af8b 110 non-free/binary-powerpc/Release 131dad26e72283d10abe76613438ace2bf68ea1c20eaa0e39e3576ba61ea39c1 255982 non-free/binary-ppc64el/Packages 2353851e1467160d0ecda93e72f892b2d1009797757e8feb6ee8014104e9983c 27796 non-free/binary-ppc64el/Packages.diff/Index 642496bde584ac91353c4e18870f21dd2c1bed5fbd8c73ccc32b3084130b5659 68178 non-free/binary-ppc64el/Packages.gz 380f53e5feb50fa52ae904a4b6f8480141706bc9dfcfe7ffbe938372a7d7cd4a 57476 non-free/binary-ppc64el/Packages.xz 970ba8cf785da4cdf0e7026d87608221d0aa6b0b4661de668a45bcab3a6bb9af 110 non-free/binary-ppc64el/Release 76c0086e000bb9708e4e0d307d307b6a1b98c8b75f1a1d50789b8c2421a06ebe 232449 non-free/binary-s390x/Packages 55bcfc0566e33955b96cc779c40837d5e40349b2c01b18133dcd6e120b7eb4e0 27796 non-free/binary-s390x/Packages.diff/Index 27ba2a8e2f933fe69b5301a048f98f046e29558ac03288a43f50c25018d54317 63088 non-free/binary-s390x/Packages.gz 986836565285be47ca418f6e4f1e6f9cc9a99fddc28ee407afe524b48e1d6e64 53436 non-free/binary-s390x/Packages.xz 952da0e4c619530fb52fb6a84da94ab5d9b10e103ff00929787bb521e338a194 108 non-free/binary-s390x/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-all/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-all/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-all/Packages.xz 51d50056fc0cb0e4394cad60c703bdac7c1a8643df381e7d011347be4b3ba13b 106 non-free/debian-installer/binary-all/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-amd64/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-amd64/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-amd64/Packages.xz d60bd5af7b9989c00aa24c2afa89d251f7ff5fc0bf9fa15e47afcea91adcd051 108 non-free/debian-installer/binary-amd64/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-arm64/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-arm64/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-arm64/Packages.xz 946c20f0df385f2d7233b2ce90460bd0894394e8b1bb6b2b00293223813d27b3 108 non-free/debian-installer/binary-arm64/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-armel/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-armel/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-armel/Packages.xz 9984e12504679bfbd364ebfef3ba6406aa0445b71b5b98f52567ca60b23dc656 108 non-free/debian-installer/binary-armel/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-armhf/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-armhf/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-armhf/Packages.xz 4a0d4b3cce6c96e15e0111d7c66a6db69d4bf177a54553b7dc4e024f780bc2f1 108 non-free/debian-installer/binary-armhf/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-hurd-i386/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-hurd-i386/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-hurd-i386/Packages.xz 881c495ebeb1c4eb5a95f394574a6114c2bc0f79b8730ca7c94b1a5d152211ac 112 non-free/debian-installer/binary-hurd-i386/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-i386/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-i386/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-i386/Packages.xz 8dfb106459eb8e64fb45eb64cc9149575f3ede58182df83611126833f8d0e1ca 107 non-free/debian-installer/binary-i386/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-kfreebsd-amd64/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-kfreebsd-amd64/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-kfreebsd-amd64/Packages.xz cf14ba1aa77c9bab4034aa54aa6b65c064aab1e21e83efd216b550e5dfb2f8ba 117 non-free/debian-installer/binary-kfreebsd-amd64/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-kfreebsd-i386/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-kfreebsd-i386/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-kfreebsd-i386/Packages.xz 6d5ef61e0b5cd01d118249fe98aa27b6bde15b8d7a13c0ce0faf90f75a891270 116 non-free/debian-installer/binary-kfreebsd-i386/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-mips/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-mips/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-mips/Packages.xz 143ca71226997dd2873eee4118fe26f3443f036fead6ff149c614e2db5f22202 107 non-free/debian-installer/binary-mips/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-mips64el/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-mips64el/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-mips64el/Packages.xz 8fbe42254ca8ac1b019a4fda52d912da4b93e95ae0c9eb04c7f4748d0bc50057 111 non-free/debian-installer/binary-mips64el/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-mipsel/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-mipsel/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-mipsel/Packages.xz 32e889dd01e87d5bc03c0ef9bfc68601c2da6e777369be839f6833faf7586c05 109 non-free/debian-installer/binary-mipsel/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-powerpc/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-powerpc/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-powerpc/Packages.xz 2f69def9d89a278cfef84fd0e096d9f1ba890c128f9b6a40c9b6b71fe968af8b 110 non-free/debian-installer/binary-powerpc/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-ppc64el/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-ppc64el/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-ppc64el/Packages.xz 970ba8cf785da4cdf0e7026d87608221d0aa6b0b4661de668a45bcab3a6bb9af 110 non-free/debian-installer/binary-ppc64el/Release e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-s390x/Packages f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-s390x/Packages.gz 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-s390x/Packages.xz 952da0e4c619530fb52fb6a84da94ab5d9b10e103ff00929787bb521e338a194 108 non-free/debian-installer/binary-s390x/Release d0f99061985c22fc085c6ae8eb3c1b49faeda978248b6dc9231251e07fc55d38 47445 non-free/dep11/Components-amd64.yml dec450f8372f0ba0292e6f59114d10d04de78aa3c1d0f905cdb60c31eb525cdb 7189 non-free/dep11/Components-amd64.yml.gz e91a6c938db083f3c72448e7d6a14588a8d8372026ec9da858918f05d2998c24 6624 non-free/dep11/Components-amd64.yml.xz 60c08380e7da73997e7fefef47aeae1cdf0d06eac41b8813c44afe23cbebf7f8 45122 non-free/dep11/Components-arm64.yml 947899ac45c47b076521e6390a29cb0703cd14e691ece7768823a2320bdf209b 6383 non-free/dep11/Components-arm64.yml.gz 8370528f52c99e8dad9505e8e0cb3051d86e01778501a5f7b522243296844894 5860 non-free/dep11/Components-arm64.yml.xz 8b7cc63f3c1a7f5138e2c13535c860ddab680f6321481c9a1d225b64e9ca6014 45122 non-free/dep11/Components-armel.yml 6d0000443aa725f3d62662c96049d7d96c67bd0cf132430e5c4e48d6a29143e2 6421 non-free/dep11/Components-armel.yml.gz aff035929971ae4118bca9cc2ecf87951270665db88be40545bf916d46feb627 5868 non-free/dep11/Components-armel.yml.xz 7c8f94dfed05924a841405c4e54638fa83befb07377ed52ac3f799db65f25808 45122 non-free/dep11/Components-armhf.yml 5f1f7c50a2457f181e7d6381cf9a99e5a0bea573610ee586e402eae2f911ab52 6463 non-free/dep11/Components-armhf.yml.gz d2ceb81fdd755aa22f5b2a32e82e4b4adabb48377d05875ad2c98db0b15dca1e 5884 non-free/dep11/Components-armhf.yml.xz fd3364d4ca33f527b0814355ff6d95bb3eeb1e56caf65c8c60017de8f46cea0e 47903 non-free/dep11/Components-i386.yml 707b8503d1752b4d82d09478590494c5b2d78591fbf75834ec9f8477292412bd 7512 non-free/dep11/Components-i386.yml.gz c8d50634540d21764ed49285171e58d31717dfc57849bce6ed1acd42a0d00837 6904 non-free/dep11/Components-i386.yml.xz 06e3abb8226856dd6655d842288920851c52cad5b644c885502157121d83e7e0 46201 non-free/dep11/Components-kfreebsd-amd64.yml af142b0fd62c51b911642891d4eaf203ac36022c99f5d05b03437b65dd233908 6927 non-free/dep11/Components-kfreebsd-amd64.yml.gz b8337441fc10262af8b3f68eabc24ced512cd2933a53404ea1e67ec8196647b9 6344 non-free/dep11/Components-kfreebsd-amd64.yml.xz c8fcfe26fa32623a4ade65c2fb740185cd488ed9f0b7eb26939901d0da0f1cd0 45122 non-free/dep11/Components-powerpc.yml 845f19e2cd67e13e23656fdfeb91e7353de66aa3e097bd40f87e3f14adbdfac7 6441 non-free/dep11/Components-powerpc.yml.gz 0a35a6fbabeac70a8a915e18946feea69d47a9c562598aac613dcbe2b1dd0739 5896 non-free/dep11/Components-powerpc.yml.xz 29e1afd7d3ecee0ca8926f94c1ef267374309fbea0315ce481cc94165d3b5790 30720 non-free/dep11/icons-128x128.tar 9aa465bbd41ea479173c3d49a216abdd64d7a7e945315d2d7316cb6cd21789de 20291 non-free/dep11/icons-128x128.tar.gz 3b7328ed6d22dccff7d283a2d72292dcce91b6f27d10d78033f37fb006359ce9 17920 non-free/dep11/icons-64x64.tar ec4198e6389982d54d353d63f7390352651d59acebadeb80b237d86400774e63 10022 non-free/dep11/icons-64x64.tar.gz 6fa6ff9f2b38fe64c81201a8fb73122f9725b00f3f43d553753cba01f7a3d5bc 374836 non-free/i18n/Translation-en 5f2db90b88c5c50154b27faf9cf308eb798268ae3cc38ef15beff9e2412e020c 79968 non-free/i18n/Translation-en.bz2 698511b9e33017225786d93c58b1fc531c4a1e3e615532617ed2d11e10c87656 25336 non-free/i18n/Translation-en.diff/Index 1fb81c024b1b640be3fdc17b17e9103eb7f7f712db540eea6ca59c24193ce8d6 109 non-free/source/Release 1f280627c948f7394f7aafded1aef1d8c0b3ebb78708928d6c4fc04c2e44fee0 344837 non-free/source/Sources a79903aaa72a8fc8f62f8813a44b1bccaa0bf690f17e30fb99b7975eab3d9e9f 27796 non-free/source/Sources.diff/Index 7719c8e965535f0df1f01a4c6bef2d05911ce17f3f5cc1a20b7f1c304ffa5934 98981 non-free/source/Sources.gz fab9e25c7264aff82e37039291a582d882550b2daa36881a572dfea00e56c0f5 82868 non-free/source/Sources.xz -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 iQIcBAEBCAAGBQJXmdEeAAoJEItIrWJGklVTTpkQALfUWL/coZ8EAVgE/YjDmGqQ o1wfpd5ffQFESPXVIFR4uJMlM/lmuST2+UB0DZB86qXESPHo56zXngPj3tDRV1tU xHbMtmZl/gLDmzISSDJ52+i+Ma8hAXUp3a1t+8QjSUf31OUPb40+mj6ft2aSJH4X 8juskoA/1v4VUCuGDiE1LD9EtrxJlGDAbh7h3xkbrp/ISwy9NC43sgsFOoEKip3m ZxkjgZ5VHFoKcSMgpjkkF/6kBhWHh7s4H8UA2LKpLPzIzuNNepLTQvWeFm/PMdcR k5kVtMTy3Vnx73wSbVblGZ6fNroO22AgAUCqk5u7oKGOn/A4cQqKxrpCvWVog1Fw wPicGkOELP1v9IsBiB24W4cBMN7vciHgssL8pgKCEn2SZVRT4SyofbwXgFckXYF5 VY11FFTXsfQXHn2o2E03sJkJ+nJIL1i6iiZXPrGfmg0lews9NXfloH3oLGobJpEd +T9X4NOWIFk8UyAvEuCQQZfkp9eQcXvhKR+Lmp4Zlc2+GcOZC+00vBlXf27hhhFF MdjwMGiTGxMFj+/CSTEMY9/86hk4W+HkDhCRFxsEd8HcCFEaPIjeEVSFi8RXbu8g YORq1kwuT4JlYA9GO1d2fMeNN2J1GJOunJeuvjXJ+sR1m7ybhxcMBYr30GsX/yQ8 XTd3OeBH1rfyPJIT8JFGiQIcBAEBCAAGBQJXmdEeAAoJEHY40EQrkNAQTpkQAJio 4zB+74XQpNoLpKSaebOmjW/GqlBbbex9j1t/84dgl34Gy7xeZ0PMTugCiYb0IipK fXYFoqij0g2n4/NWpyxNEFI0X+EWu+MgIAPAfFgZIbpJourKSc9NQPlDc3z4vLvW ylIZ0jxYfmwT/gaDw6c0Eq2eLcoi46UDnj1YdByrGfN47BXSzl2eMJbHwFuc9f9q 5XUSOMCPqcPqELUlCOmlp/858dE1ePCnaO8/IP8Dr48jpJhzaUtaJMdYxZ3mbog5 2mrCs6a9tn8QmkKTHZ+X5HwA7vgp9Hxt7rB2371ZnEdhUj6J3GsFSnQ52tzU9nKY 4AJbm+oK6o49dT3cO8EXFmH6z6F8lZXvAq112sA+5E6mLjhcQOMBuTZIB2l8Xav7 nkCtZFkDaHs8h1Tafs3xneMFU1N0zjirLWQ1DLakYZf74L2oyis2BLwFnKWyD7tn ns9VtiqgrWhCAAWDcLKCHOEVUuYV/7VgxvZHRiyK/SdUm3ajHYR2WqDckuuedcTe BHoUOiCQ9nVbhugYZBntFX8wKOHix3Tmp+JCnM4SWtwGNu/lwjZMqwIxvKY93+CR /D8cCVNvWplKPgmPz6jGRg31/R5Oq2HuMM5iByQOjk5dnXfy9oKBjtD0kB2Fa7Pe rWqOLXoUsaEsByI9NqinUBxI8duCg83YIrXZDY3u =+BFo -----END PGP SIGNATURE----- appstream-generator-0.10.1/tests/samples/extra-metainfo/000077500000000000000000000000001506754475600233255ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/extra-metainfo/modifications.json000066400000000000000000000002641506754475600270520ustar00rootroot00000000000000{ "Remove": [ "com.example.removed" ], "InjectCustom": { "org.example.newdata": { "earth": "moon", "mars": "phobos", "saturn": "thrym" } } } appstream-generator-0.10.1/tests/samples/rpmmd/000077500000000000000000000000001506754475600215215ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/rpmmd/26/000077500000000000000000000000001506754475600217505ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/rpmmd/26/Workstation/000077500000000000000000000000001506754475600242745ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/rpmmd/26/Workstation/x86_64/000077500000000000000000000000001506754475600252325ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/rpmmd/26/Workstation/x86_64/os/000077500000000000000000000000001506754475600256535ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/rpmmd/26/Workstation/x86_64/os/repodata/000077500000000000000000000000001506754475600274525ustar00rootroot00000000000000359c4a0d13731581be86f571f3a672e82fe52f914f00929243f17c16da820326-filelists.xml.gz000066400000000000000000000101341506754475600426730ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/rpmmd/26/Workstation/x86_64/os/repodata[w:SxCjά4m愒 9/gQ%I#نpKRHc[۶_qT[z٪׀yܧ,j AD5}=R?',}lN Dgs}oIBiuTZK!/C ng ݠw 7vaoxmu{`kNE0a{4ND*$n@EF? VP 7ҹ?f^w1hN7'q)s`>d\[SOǧ'4ݔF~g]츃_\osZІA F}7v;~[l sxwC.Ƃ2%?%g'%ZPG\4bhPc3~8.%qBDŽc϶ޑ(Yq<_gU."6Ԥz2E4Rg*RTyW)h\F楖{rz=LP(͒OOǃ_^>N/PȽߟ.@>5fAKT0vC%oCWϒt2<7Җm:6r] J[X{+'uSs )׹?ޑ{=n&f?c?%|z3="d+ג9w㯳:Xq|v#۽;~mwZ?jۆΰKFx^;zꕆfd ;X8 [%ym9vxeg: _Y-7Jn7l+' #H47U\/n%01 1$v[ /@ioyP?|Ply~m&|z{楊F"a $f $cU8(S a+/͐-̿5M:^YXxtU@0Ÿ5i#Tp[#ΔY@*hiYBG7 p1za3tu8FľKH +vԱ9S=!W rx!6(Sd/ _ h}VEY+Q`5 Nzz0drqE6 切exs js=CN-~ic>[Dĝ鱖+rM\ْuA%&͓̿O̻խ`Vg ƸN(QRJP7U JĩnB'h|$|mhc({̎OpFA ,B?3j`CB &hPύ=LU9 0cwH3=iPAFsd1Jze;u a̔ӶΑb.*U7{k qؠ4([dS̱6t\`$ӓRjWGzň> ЃުqZ b*q<lMaM. pn fHn`0<艆jBMvR2X"Ogy>,Wc9Y.j 8Ⱥ9UaƽeУfb5'"=ϝ.*M& aNQ2ga].cަOJt0T~q3/=@?\u:Rkn(^tQa20c2@B~a:Bg%( )N9`=ϙHIR:j:s{N1|'؄?8[bNf W"3EP8n1pF&L{D5߉-eѼ4- d >68k=H#J>ric2IZC#Pֈ5 y^ g,"vFȟv֌AYX}bʟ\7~xPA#3[&!>0x 5 ( )-{ҫ0% VƩE^p~Y>ʭLAT#,}l,]>8^|-ՒeH:{$vnְ C7@kx@^wкF݁;jk&Ⲯ[if^2zYg\3Ӛ. 'BeݼD,3ug]ʜ\3ߦk% f\h_ag `t^Aג "ԥybfk1^O~;\,^6f+|/9W_.Owz|a=a|u37qffڒg'>;YG8n|57N{NbU9g5ws=oGL 7ÿO(>8.b߹ {ѠtAKoAv|~0r=uA]\yirdSG5{I^8;gw1봑f1fhNy˚shy7&$'<kzaCՇAuag}hI*==L4iQ6܈ZZ#oT=I|i#r% 0KGa=YUs+y$K\Vj騬ي b`omplu[t|reK QABmcmlc(o\&cǚA~Y<,ʄK[ySoiC*٬>T*>kO׆eMUHd#;zH=Aӆg,&ǞE 6-Y87ZgOhjx%<4_DXsȕ/\ {OЖ7DvYs-dre5yHpR|Z`! ڶ0me&j@lV}DTE\Ƶ担<4vWs~Bz*i볐ó;wi6ꦊ je~\4H8WN|>wʷLnjKmօ/kp>q>n%;z#ӻISkgiϺL͖m.evL ,h +fʬdt}@v=T鞺ݿ891148a3ce31e0695aa00ee72a7730e6978b049357956c81a605396453cc200a-other.xml.gz000066400000000000000000000013511506754475600417350ustar00rootroot00000000000000appstream-generator-0.10.1/tests/samples/rpmmd/26/Workstation/x86_64/os/repodataKo6 a%I5^'-P=99ԪDA\a#}SVho¼ti<ѧЍxo._\|9ڏ˱8<}SU}7weXoVdYU}A&o\ h/ rJ zm"H6j)5UK*U-Qvсhdt 7MK F]Nt,[lB.UҶ/x/u) 2CdE?OnlO-qk>%4\O~H>mvp]_4lkr EOy`L֖ n)-.DZz/Xȩ:Y\#A(&Inq90VLIiF/4+ 墔Z)2eBD8>TE% Cc)75}ׯ@̈3!V:gtHL3fGI ^wh19i'4|,HN$vwY>,K&eVrzp GL`*C0on57Ls>>m-WЂ*s',Oӻu|? aE0I-ajak,)Ja{-.jCkTVBJ8!m petRT٦qWݣ^o$kGW VMbUݰ>!ZZ-2i'%+Zyb~8>3+U9p؋%kv Hq{깤Ѵ,D4a7(M};NLq , }}8\\-iI6ƗۅM5V,ctt򚬵O.T=OE%+:kt~MD#S O.CL5r-? !tqDzXۓ m PIdajP"Ў[gCꩻtƹW5LQ2r9{dh j*a:/aϿtLwKAb1)fR W KED6R'ƅCDYCآU( κ ,en&3Hk(wca IKb1 X 'UD(g?TusZ.h#UcCceU|b/998٧owdP.+n/#*Yd>k%lGҒ?]4f7M+3T$M)N<ؙ'~Dij ]+H+GM1=/"-zBZ-m!-5<j]y{S*"c70'?E뜖sI ^kVE8S~n 7$ub?W4ÙClVͶk;V<ͭwﻣͬbw};zv1>-HݐKV.'pHҹӣR}B{2}wQLy'&;ZC5XF^kp{^b 5unf5;\ٛ:+p.Y:e9ړ (@.z,O°-o@vr1$h'+s~c^0:_9ޡYyySGP}; U-LK/m{ ɚ>}$O\_) | Y N3sON!?{qݻK״W(CG20abEEKǘ;hC_Nr騚_&/3X3n(it]x uLa S,]'増s;ڞޛLIN8p'|+ 0~ o|=\ %QISߣn=igNVϯ3L{Ïá Z8BϷ"8B-0F{访F2z ޸Ԣ~PlTyo 49A%appstream-generator-0.10.1/tests/samples/rpmmd/26/Workstation/x86_64/os/repodata/repomd.xml000066400000000000000000000030171506754475600314630ustar00rootroot00000000000000 1498315710 9e6f400df7895e52984c987d7fea543cc9ca9a87f43d5652a51f44241aaeb03a 98e300b6683be9c9e959ece52969ee32c0171cc9d922325c57af58ca2c9e4a65 1498315710 2285 9537 359c4a0d13731581be86f571f3a672e82fe52f914f00929243f17c16da820326 37006a808d524721a0e42ac402cffe6ef9c503478985569ec52bf0ed4315695f 1498315710 4188 49117 891148a3ce31e0695aa00ee72a7730e6978b049357956c81a605396453cc200a 84e64f9167328898522844619599d962dc33c3334c529b5bb512e85e850a0f1b 1498315710 745 2499 appstream-generator-0.10.1/tests/samples/sample-video-credit.txt000066400000000000000000000003201506754475600247730ustar00rootroot00000000000000The "Dawn City" footage was authored by Dmitry Tolsty under a royalty-free, free-for-commercial, no-attribution license. See https://pixabay.com/users/badzhoo-5607787/ for more information about the creator. appstream-generator-0.10.1/tests/samples/sample-video.mkv000066400000000000000000007410731506754475600235220ustar00rootroot00000000000000Eߣ#BBBBBmatroskaBBSgMt@BEYMSIfSMSTkS%DSo] e;-NRAI-,Fc 2a6s<| AN0Ǭ]*K+Y,*#5'Kf B X$mb)@P$-)zhڳƑr4(W:ǎVX2@$qY:l=욮WK9(D69EHͥ5-kpk+a?;S\!oYʇ̎Aefߢ`ß+\zb6Mx4b[Qe#$W.E6]iTN\[^U^=+$‹8@GqE?_e7_w rf '5 bJή*KEt[>nq7A>~h7ɿϬQ'myd-(9wMWz1`C7d\DlrryhC1Gi.e[Y,Wn%ҰJMPJi~=ޕvBascf%"AX1I66)YT|3v4LfUpSQj=61imr&"wy:0Ue}*l?ӵU N{S"Ii8gBE-*fĔ XVlEa$ _,Ӊ^҈dB $ܴ3*,&J)VRc*Z?MYαUz 'z'ῑl'Dcr2V-=>2[Z9 ɳy&yS ѵCV),+ ıa#'_(ehT:4p;$a4kI 7*wT^ԾP@=BQ֬[`JKb %~ 7# 5JG|bV`@lS<ɉefn5'}<"./Vc$.aCebx*vPnfPC){k5o`P _YJ#c[Iaƪ&<_Bfṕ6b .,Qr՟U{7bɎ :@#Ayh[͘Dӡj nC" \ّHoW:*RGHI!&(;K{N(2/( kT9T<'nCD%22\YYf_LVmg_+6&.|HRo߽7RY._QAspcȬmV&JOMAY_2-mYB^xkaFdwÌv*C̄O^M=#kz@uξZϵʬ7#&JzaN`ZU0jw,V2C>r%\ )iVo rXzɥsB'.z0Z$_z2P%U*ɣ.r7&rGsǛ\~lE..nL / ,\ֲDdk"##h?AfT3˩Z>L 9Z:mI\ɏ P Գ0y3Xzk(+H fT1VJ~\Tr<nA`?3G"ʆR(aPB* ɰV_L=?R6ANC' ҤaRUΰ9tg*b.7;,4V{3_R$Eda,}3S$M"u_y@?}(ZVoU ۪-q#p0=fiV$38:eRGqT_ ߽!8y]#"d8 U@MΗ 3^lȣ+6~72n! JibM~bM9B 8uʓ!.c"1Ҥlm k@1bJ_ HHB;,F≲45Ј6(MFxqy%Iq4={6(A`5 w`^@䔲Ue3)BrV?!dl:b[Ux"Di}ಠN}_yHE;t0Eѻ]Usײ|rx%;)_"e ;u- B7=& _,_K'`U>tYb!GuS){sOc mէBVD$VȌQ?x$z,M%6a{ry:۠1мy{hsv;o0)ET%9?skIv_T^7ZTZo+~ְhZ4KKv528&uƱ"ֈ3S61 n,*iղݯ(c oASm*GT}cʃTFhT φHVj΂[AqkC@qTQ~uF3N w€u7,GOLo[jҳg_hIr&nVt- g#W5j}YXE[ȚDAem1c,fGNlGBr,9c>kp8ae˾Pi\Cl4m nhӜLvtedvt'!(clhcdVх Yϳ T,}{OnYjbgqIWt<ǴT-p^2Aݶʚ$t)sſ#VTݑC N}6uWO}y5UVIpŵScKCZviWI{ˈX4%++Gx(Oˋ񄟤ZIGv9֊ ӣ/TxOF.dX' 0pE(\{/*\*:}S`REYoS+x 'E>ڦԥ8ʷZZg0h>LX9ʊrŶBIB}%~F<nVCbHw-UĬ|K#!}TBh`xK@Y$FgCv f!rſVXӾը K/ӎMBQJQ[krw\ϾYgr&9@ Wǯmr97w | M>Z^ˈx[od[f?+ ]~zV̷[D Nx&r&1zr=vl t44sn3QW#y+ŵ`ϘHQReBR IzhV{YL+ߵj,γCzz$Vn *+@5ukCCix+[iPۺR/9^?jR9?jdžJ#Tm g۠VkE;kF:!}=/Ih !Z9= ;tz@ī/*Tp{˲@Ғuh>'>ćhXp$t`[Lxw.I0sP$\\`@q_ ;ÄZƌyc۾4GdY@O 4@:[u 4Y|ߡ /Πo|K': \pc<sә񳓀<jR+hq\量z;H/L!ח>i*R6hI_\+L(Ic Cqmo2F8Yh ٝ:.Ȳ\Tco6-|9iVz˙>S8p usJ[Qۓ8P(#0Cؓ'\*:vK2B!Qk͖$W:~xI5+&h"w>ԩ "M\"6R9TlK]Umࡆic0R4S鬃P&L)~hFeiVS(4@ńw%r/S n:Oٱq :(!dUb@JK>5dEա1 K5&l~1ihU_$9{7KR[>S%R٭a:ALMm᫏7~$xt;m)˖hY\K (6* xn WRfqqj];wMoD*ejW(8 EUX%Z#lħHvx+˜;RKbܶj'cv'js-Vf=/>N|c_f3tptL\O|I'5!D(g 1 -)dZmK7VhŹQ=hEr1K%AfiA} OZQ2"F K '/k3>Kc |_̿y0QǓLz"7 F95;ԖJ \!S Ȃk 1qrwHHÅ 3S[oun F~ӟ_Lt0+@K$teq%e\Q'I=YA*lp#aƲKmàt$ZKjaf4@a C|=ՆǷLrԠ;l åNIY%!N~'*M&̙S-Iqfao.²%.'gc_ËnR1Xjiyx=Ǣ!3g~5\76A"Nd]vhKwV]]0qgLͦ0 Fg3ri8É|Vk 2{0s"(j8T50)ՇAi Ga.tBGo#6EɆv(Nu68<.g>#=$*weU?5{d`ؼ%wO;dѺʴψ5yQ4 X~w|XuYoECMD+Nv1=1$EЈBu'e\ի[od5b '}|d#rCɋT B^ I5rnZIFe$Uh+o/" ld]BAPa~Ŝ!T"Jw[tB`jKWO#v*ڛrnp[8\TwPAR,l @"z=h̘(k=`@VR>^IMMmUm!).K F4ֈ,VU45[TNt ,JpK"-^e~S%~C5/mpeY;ii?` ֥ dlIN?G>kV"N*nW9L 7Ql|Uuy]./FH"(" ̋1~ү2Lc A¸[f< 2a~<&r-xu9a4cM.q2D5B^٨!zgHcZTU$"@Q|l\;~Ďmfz̨ge-^V¼Ӭ?P 'JJɧԴGjpȈ;;*!XKpD۳;@AZ8}kD< 6V>S$v__u;-i0Q{rgz`dRSpzV,%<cwN;ISOUɐ~WH0RQ{NQ/^ jg[ ~%̫Mi^ A96@݇kZ5>Ԁ |~g =ub\ +D!YɱF!Azm;pIM ͐CK| m:C s 9\`)3 Wct (8=I%j壱A3He'mý7>dVw#O(~ĒPO. ?T')[|0#MTE*# #5rI8W%yx75;V (]Y6D;b顤[R p{t٧OA"UCﮑZ2ptUI: ߙ3Jw_GqP)[q_w CeȼJ63:чG!6J?`d&ߟ{^FWRR6whd>MQGOS?!ZA]YʠzA kl2Cg״KVHC S22D ?@@0 VK#B׷ϿJ%j_様ND-60)wZKbyܓ' EjJ, ^УU%a6A>jc~:@m8Mt u_3SRU3Oi\^ J#f")L?[&OB?¢nj"Fzp2Mx4n;:*4ĪIGڸ tօWqhSRXFrDI;: ㆮڼ[)a3 4x_)*2dyU(ɤ RR֒x AءOn2ا'فdv'&XTpĒ\m.W meP+ ĸ Ωثb X'ǔ:AP ;5ɴTˣBՁ22S?0B v G1N 82lշP֫串5~Y2v #( !X뇑r˒[dpȿPmdB9o=Ō f߿-}c'eh2ϲkXJC449-RiN߬tRrF%fHY-<j.t݆FH"OtX_V΂`ҟ6 Q"{c^*Bi罚,>ڹ!W8PIҡpqLBvU޽%` ~%!\Ib4! {B  f]܄b"A22"?@ Q B$FZT^$pCg.и_fTDm8``ޝ{$biFb|b`?Q]*0,ΎX&`"=tA6YrЊ p `צ]qG@1L+&.ޠ7kTtO r5Ql)knف !f-H|7]`&{ݿ(͡T@I$ wNϊ/c6o>G>>0 y}p{TX'`NH?oS/azD|%G@V7TPܽYIcxK >;B?F4xJ@m3 /h73wPϏc<;t!oאg5q-tLDbeh,cJT6&H l.H-ɫOPIo{]\jr8W"@=t\ ᢤije7…G 14]OA.P7ZY8ʭv Ep!oҔ*:~SҦ _̘m }!0IJbRx뙏Paڨ3' lu^hk&nF5R*>vEǯH6m>Sd[ r$&VFۜ\# ;bP|8{w~D(P&4oPbʵ Kmiac`AaA݂%08Ǎ/:9br_(Iil-pٔwToڪb*eӶP w2!J߆ ˆ a @23y· ',/\QUdNj|ًbg^OWs } bq-0 2dYgs{" gfu󺋉۴gm Sm72 HrR %Rt(f"cX -jI2Rǥvb~d0oei0e4WyCzϊM<`mRf]c} CS,W郦GT!D.GvYc=:Q~vùǂgW;6YH~C <0r Nq/r R,ldgiKJ2P{$a7tvǏ"۩IԽ#`u57Ny7Z{n\(r.W ' EʃaƙoWbX7|#B)nj 4Qv"*K 3NQ&gULk\b4bLs`hS? p;um‰{ϼX!~ %*jݐD.jqR&%12+lI 8r4Hߠp7"xcƹ.}38n{Hp%)5Oa (TҊw-Pˆ l@-`Y!&P>@+in!Gd u#uׁPA!}(m&t/a􉴝\nx=8:윫i$4wږ@o;K @_#Q}8Nq!~qFrPT;8ϫtq4{:SS:5{Z3'|dj*z ɫ+q, .$dBx?5%('IPYNC;kjiLqqN>2.S:as1R .,Bpqsy*( nQu/zT跼k=ƙg%yUX cĦ+ݎt|$H b(Bu,"h(-{9E)cM+ 0yf73q/AZ=%^WolW\"흦Aݟ m﷒t[mxjlZ**]ߍe//=XR =m6#ͅQ1o??#]nr\l 3sד\Q: >s"죫v2&|J.z!1-WL0^[}z!(E7e[&?EyjuCI OS,(ǽ M|NޥJh/ϥ\pL/+]3oD"Dv({].k8@7 )X=qQED`5V }: iUu 7B9oE鳷i=WpJ-OBˎ(ksM)SYe,K0e\E;xEY ۨ}'tgefP媂ֿ6An>p9\ =wqZg,C9ê-p3b]A'sAK3% _"v0խ,)}AT2!?7<,u+[;u2(>rEh!_)$:t| uu 䝪>NJ{L^MZ&D2:Z=\%`ɴǝd8 lb1! Hv;BAȍ~5tyEؚNN'o ڇ;ޅ[̑ODu9tB!rLWy2&$P kjq,U+vK;M)gGj>sAWJjF+{|D'Qx<1w3]5͝2$Q*4p[mR*z0Sb&1\I בyUTGs>Z?w܇ZQp26c*U əM֔]nyPl=sĜAM 7q3JQUuHbu0ELWzs,:h ITV[oKI~;ϝ5G9AUQ@B"lAL `Ll? H!aTKr51wo㼌9q<^P\- ޹֚WrG&dðB Vy'7\t"lYc5+\-)**;%չ_0L!{vԖcח>b]xiȿ NDQH% ֺIcN o+{ԖNM(p? hԌrF["!oh3kitSk?~*Tqgmua!=n,7ri-e>52@v'>T-']3g2JhHYUT3@H< $phoPMⳘλ 5{v _7@m^X(OE%+6ԨDʤ+_=L[0C(Í-VYe#+d4^ ׷ezܗ*,9y+5gO8&%"acdvCڴTv`TXVLцwV+|3߳Gqïo^MlwFBKMC8gz+6GHę[㻤>^PPwMH巩|B;G* fھIk.Q}W261|Xstq}T\Ys(@i2l^|FJ).iă=d՞/tM`~^p85Ӑ_<_%!Ȧ-ˋ6g]";b*5OrWXOg@ P`(G׌DCGW/ʶQ *37;G|c-N}ďll1xW/]Z[%3L H͡ka0q l ~jF0Hp- J MV[6<S?WxUtxYwQgx ,#=,q1#b:9;DZ7&>_v,mh(,b$fR}NQWc `.A%Xfğ2P98'bZZ%bN"AgK D^AV0s۾Tĩ2C p+M=RHeaUvcڥtWdao.-;K+ y騢_̈yM;46ƹyj/J/e&/ =qNНU=hnmQ6w7P'x. tžl$C?4e Ds22 "@(qB[@ `Ž&8Hrbht_$ oF e0Ɔ%oP49S)Ik#GRCהOD=OWgaȻ 2nh~P[ԇ"fcz@yӋ.C=ٝ62Vbxh@@8'{%<VT/Qa>7= j+zCj6|C|h)U \ݣ(IbYܞsY+YSvÅ6mx6ݕ{U  󛘰 c9CYwڳҾ!w$9dHۨy]]I U0M)_J\;m#q^%wd2G~۬#@N&Ж!vv?ZL}dF5(NgV62?_qpVS~_1jnva5 l7mf2v7ӝ:`{GS OQZ3dkBL,jQ$4-7>!$6jJVR|Ic:zݧo] 5~ \)2D+meشWEUI=U CSV&/H-LoyS#~iJ 4QtڐtE9pV7w$hv*ڥn#mB^vk4B22 V8QE[h MUD3NFCE[N"KQ7YDT]/֙3 {Ѥ,Ԡ㛀u%@(K`ߺd\U&|4MOXGKwak2Y!)tp+ϵtI(Q~Y;uvl(rʯgR 3/rEQQ\|WK~ش'Lv:?RܙC(+t ܰ d+@/bGhIT%TCC2rf9̀jͩ[㏂?屜L̥u62'_3͆+!mcd2h4!6w,9Mq`ȭ`k@ 2 60įf{'S39"5/[t˿:=@A6hËOT Tsƅ=&=2d7x% qc,6h\6p\Iu 4# }G3  :c$p+st~saԣD222 DAGB(bi!qgAy`t̳$RROW桬TA'6ܲy|& `NZ3^M.u.&K)~M:S;P/aJ#9ϓbj q&1-]NQ+t ȋ4ژp.,`|_/^ RB3eYJذIٞQ`P܂4YpyŚJL& 4m6 ; c]쥂:j\ kC>i&+M2SȾ83 pD=s*T!<2>|qLFa+NuJTcyxnsEo_)nÅEǟ?P4-ų:%ÉN,EmflYCUMCbRc/ @ѹ7jjORD5&D>ʿJ2$b< NvQ8xMqUJ &l^Nsd4=K*HТ7|,ġ#ガG`=Zf騹#C#L&SSs%I.IhEPxv. i&g B22 "$aBR.dĉ`ؾ?3eW[jsP/k %? ( pү.Ʉa약~IlSQ. Н &_4l$EzB!zC3Cf7kbP.*=fTx.+ B7–yׅ{s͌ UI>C+"Ay} cVig>N셌&j³gpZyw֠F*ⴘnS|b! (Nb%†W?v4s[Z+9Sۊ aGroLCxe:1RHɉ\ xB$TZq!H1c=2 OնO*=Z)COxxz;I'uuqoQDmIҰ9H~?#f?籱/78#X)PDEO {fjӕӎ"tG`CN%,2i:_s0#6:'orh1CT_G 00i\c{it?N~j.jj0?Jq:jy1SQ$CC>J}v:l||^Ywה#fJgN >/hbFm~qRAv1gZG ڱ8cNn-2!V5)Zl's ybNg;L}$ٗ>,&`:RJ#6\"b4.2QN1هhVv֥tZ{]u>6i7ƈnb4c&Zm}O5r n i^^.NG  GH!:xejǻ5n3<B22a@ B@_j+y%3"X01+fGOM_@ @6j%`>CO44J2晰Ɂ+." 7 ]l0RW2݊K+]c=(dr#2tR*]k̭(/ Cg Z|_54T@d4WQ@|w>JʈD\ vnT2!0 D^\m3$9_-Q!  >#xRMPȬW160׷1b!|ןx?4v4{LxR@ႥG06&NyH5om%(6m9NdjwF؅6B$ɳz%C/Bxu Q4uAHl٘!±DaIsؓ290KJM v]L8Y-=!-^I^ݿOdd."cbgL|KMi1Zp['1 ',]2Hi%~Ez1I}/(z:'':V$)j d+Q#^!.ڢ}ks{DT+g5h+hhayZO) Cu?CI 콄x.qfyZXfU]Z@$Mzl@I}!0߸gk W64MK/*qW(qy{l@!d9T$%8T}c>3닰\:k{ dwoDmLUdz*u1T/h!B؟<~> v7<h=gڣ+ ڱfmdM<+,x{đbZ#6+:ޱJrG!ݜS\ѓ"D0Sa?s5GLk5ƩF.Yc#{3.VgS EPI5YOAcb@ϮJ(t] sYGϠ#G&t[ga>/ >VLڷ32/p5D QOnC5$8f̐嶝[h&f$`> EQ:~ ׋VGmZ}\l$'TڙF3(✌@h)$5{6Ӧɕ-fϭQ+h*"#eMmoHcc쯫yA5*%ڶnmAo9zCX])i{aͭ*sI&EȆ_mXChkу wH=&!V6x.\7l ; = X\:@i[Pt cEV]]pـز^`L̵]X%r ǀa7"DKQ9"(m.њeI0',i C49NAeB~vn$V/!z|Lh94Cx7~"1tv_B~xc|&yGmgzjC[c`!cK1F6-4 'r%< gЛ;W3> ѡ1# ۹&wE{O߰2%Tse1l~x|4u< eD4}]b{{xc f_&r傔?LyGsmVx/%\w >]80/4WFDwVEf'lla>>{m.>ۼAԖp sQZkk'C&+_Z7hb! =^j9_!ȑ")K +QrH)n2[‡&?+l\pp#pmȔWX( 8ʴZqh)&wf ;`5Drڏ(:*zr 5dXmNlf}y0Ib18+QZ[pxF@}#/9utò4(D/Ç<^i`Q?}IA6dTGYE?Md_p#E(ZLxޒW|o. ADF-9N) _8RN3(—V{}8 4otiEwCQycJj?,z< ƄgM@)ĭY\fكԈ'?8#VV9R/B//@vYfQQK/:XDz=UG"Y.av6ïU֌oޙ$&UR ^&mu8TǕX:fBa4 j7 4!$;W:;mJć>wP[ZM(qyZ &|1-oePhN`rb\f&] E5E->O&S~癜mՂ<e'CRWi>FrVr<ˈ E+ծn5Q 0GxV#E动&ʼ W( BՍӇ6 e]?6", G)TȲ› T!bx`Bs<ܖ9@2 x*9#6q]- {Cw#kI inkOl`kG>|؍-fW.?vЪ&&yvǐWfufn]+A҉]3 r Z{]=pk7TlFEȥ閭}SS(=d?3of@5a"]܊SXpb3K_y *L }KD'22S?@ aY fɎ†kʇ4܅ut2i{Cn=/6&\oQ61o~qaߔhϸm-k+!;".'1 _/_ct*,HxIs9 n4:t$JenbˤNKa5o~gu5fԒ]AoNbzPwz Gؙ9pdhp~(c}dK{4zϼNC7aGtr1% f;="La @fOjK!V"S?.R?@%iy;&^LˤVMRmo2n&EVD>k־]1*|ŕpX<W?c2ZqbŘQTwwkٻ˿:h1F\lzV2T1.U E=c+t2Z4mMnκ5"7nTfInnJY%cژJp51>1:G0#f*LBy6h}S䵥.XXhx=%';Һ?bzĉC}ˊzDm =T䌳0e7=KXx//thW'ln>Kqp@!*.ِc3.V#\^&WfG9W"x-W=*5%,5S!$ RS'[x<M赧 ,D+T&L45G1cΗp nMB(d؝pzq/ zv#!qAn; F ZY̏ϖ.m`o$]9\^o# dRE4IK8!1M2/-M(Ё,ԃ\:kx!g l[0ptq5&hvUyWznlݺ c@@70ɛ+?|MvN:?;g@q`qvev&O~$2VVWoZcň̸I3@A22D ?(0ebT4`{ڛl¡1N}GXєp5+i=u)NDl`{pH1F+oXL:ZAj1p5~se n6 & ?,%?rry>$x/Le !C7cg*YBN:BS&6_w] jTs)+T\Vt[^pe"0}pc5QWWhaէiŒ KTW*(a CsT# 徰 pt?Bi#[iL1r]A#q< )tq(^a?7gKfmTT1DݢPE>^J޸  ~XyE2; pȃE_DB2 2"?ADf`*| 8 fvJ8=Ƭ8]?(v {hXoV7ڣUܙ n5]YZzoX+IIN<<6Ց"`]HBFi_? D3ЈcI#Uꌖ:jL\B S<w:&\Dj>K֥^k`Ԕlf) .};M5}d_+YҞijot|ڧ3P%i|ZV _2ݨ#s[UسH8wΣYrbT U¸ /ݚz,U& z}Z< !2}OA+TP_挳F X$"hC%+CQ<< n||/}|BEpSikm%cz Jz[wP-!'JtgtiQ^G@a_3|m4'vXͨ|MxLZuB)m]elx:1 /_U1N&\x9 Wt\3FF|fV$̱{< t)CܶUh(g#"=caQ>>exq(Vq3D<0tu+G5[6k46L# I7t1@j- $B){ɹAR_4!m_D`G|t$#xDs}ˠVHl!o!|^8E{T8H~ǍH^MR ]8iN+`PO\oCz]t\jOEE{N1vwC 7uC;xh8OSyWA38>x#4ڼ `HM8A(i?SLY>O8L5Y"u%lXDٿjA x)LK\UҊO, `?&Vu v}q>O&xě\ 6F 6e5[g"ux[&KZ:IUZ6ʧ2J R!5sĎܣ:E˷ W6B|ҘAzh*|E*.UkniK 0MV b$Zj"&Q>a8Mpa4L~lKCR?c3}ѴFz2^+' WZ^!Y!$ L3FbPI,Gc(I;\!.U0OuW2jP>ёM틼`5;zniƤdqȉ2Hz)n+r{9"hiv%8 9X(]@²WXtgeCkm&tyFom%I~p"ɺ~`/ i eRd]&B) ȴҔ(YJCk/! kj{9R)w3ɉt+/zaDy } nzkWrP t܎4ZC(LdazktEůj@ٙ[Na杦q!t'1HgqrVVL!^"K%Ař2Qï0J~(p 4X&ERV L%(lq@22"?0`pG:ۉW@T_IZNf-|\Y!}KܹL΁ցA=kQFvL)9_1JH1AXٮub@6HdKϨl]gaPs;\JTmAmh|V27o3`ؾ9Ng3ŏnz 7$vl3@6F–6ӄy/l7fq > ^ }3עT;PU'+"p,zc.?D\XH\LL>$uvXF: &G5yTrAbM@ gW+GXЖhM؃LKFt'LΚwz5\|ҞfuQe<]-H8}_` ưEL <ϑL-qBp%C{% pS;ۙαV$c%E>RX(=abMK_ DD$VŤ }\"HZ)8^b{hfK-QWgZS8C 8) P),owuc{T/KQ. IJ֮Z9~mCʷEG=ELTm/&aQH:Qk2! +J߆a0 8]M&R"QmbN(rrp[>yPό]m̆O<]nZx pݍ׾(67cT6ٌ{Xov{ESKRg=2 o,ZMu̓ cg#uxH8fXWq҉t<輯k&o)j'' '@oL,K<۟?$Lݺ2IH݀/z^c0NL51-8Ph?oP{bQKMx EB#dUy@bRL:`1 |iOqD'mz<ÎG:6ItATm/M50p#b¸4 7|z"E#unvkwtLg51(W}:QH9g5nU.6ꀃ25x (=p(̃Y*28|`WZ!W\A0bxa)+Kpc+7y%mlQ嫧|O=bIыlJ穢vmHsze對&Wm-pJ]߬([8 iUƨ6ebaj[%Fx*\2\,&u:g6'OSIf=G!zgGܬ08~!apn6&".p,p~Se-[.'1hq1y ܪdw3U30W{Dyљ]vR1#1![ qN\YOPk Ÿ8Xкzq3j m4Ȭ>X%z`ST>sUUpuvZ⺏ؚ6kBN9*x`,PO[s m?܆9/v@@] (F0rܒ'$ImVWs>PQJ"N1~= Y\yS0F%RjvTp"9p%b@Hi-[Qj 8*)Be=4 u~̩}J{>hlobQ!6~72Ȁ_<}ѷn41m čGNbJa@O(٥#ϩc4%iF9w_+`Px 3PZ!qw>4=s<vR]zE+>' UJq>O W=k*fCzKOkw.Gu6lH8\@դ3=m`fBDD d bd -gY `Wػ'dxC5q:b.|(qC]L|,QK+9+^/#Z5Z$fnSq^?(҃n 7/w< z%㩒׃aG%Gټ{Y0ũOH}Mn ǏzxPwZwzd+}mx:x(i#g#jg}^+D{XDE%0S?o4.L[w!Yj~׮4%ꧬ Ն3X.sjk*|L͸A*j:;Cd+{#lS<~AvCwS~9 7MJN|!KiV {=ݍJnvxڜj8،:_xe3{2Ʒ;'Giڠe:hLj|UnO΄سװڶ5#Uap)b)]phGDR:1-i`+WojT[mvw:*[SK7w甅xIm~ǰUpE\5h J_-q⿎B+cM`(l0η5g#]Ex$8b]ix(Pu}Px Fc4 x5X}ļ\τhȷa3-4W)PE3ڐ.l0z *! fY UMK47d33 XVC(E38K:,;>x`ޡKa_K|ipju_f:>M@|2)D_=Մ_ -@LO*!lhQIFUpz/45;fDawF:"eXRp)驡"D h8aå f%x(WID[/#c Py2R]-$Kq$W"L樮iءa3Cf+_|28Kc=֏ѻH"(k2{o.9 "+R` rs+Eec#+>QzqF Q~@'4.uQylgF#{֦[^ PUш{PV ?\RM>H |@22V0&79ONNT0ƟݍSь;^M2 ܟ)tݳjKoyT)h>!O5,yW1 f/W3vど'Ipӿ\8I-^Pһm/r #y `/tV<BgJR|憥6OÚM*e¥5j.?EqNuB]ϧuBCq0 rdӆw=(p\%OIө>.:(*u Dq%hJ/c)&#"řӀ_&rmB,'K "T*4hnseS1(# 67yҩ "ͧ!, al,{fF*<cNccX{lr^{/,/\tͧ[MϽK6,a%l*ג/03P\̓9' Brǖ;z2"ώ`)b惠!{UN?LNB̡H3o\mY{5S'A_D,<22D@$q8`b$@$ kсʓg;lfL~~Hi-s'iҲ~K:5B(%)Ե0gfT޵qU}@'ƨ|>eW%Buxt\,ٷPyeB 8qy`c30T{UPK%[RķCFe|ߠ:7q%i]߾E5!ق1 zgFA[*hT HerPJz/X%ѿZoQ:DpQڬA˔bC=P|@Ӛ p{P!^Lc( =a )OGWOGg9VW4\ѷxLu3܏ዴa2=8;~8g3屯vS[? K/ucb'A#|iJ Ww&/剗BP qg}UZ"uЍǤk~?AWPrtw0:=Jg@\t .w U_y(( N[pt gل-bMWY}OHۍ/ k;;)-_*L3[ۡG&\ZfXz'%mM97$tS+-|! kDzSJfRoÃ,ŅvӒI$q *KƴKͬWDˋJ>>v.h[KCkŃCj B.4 \eo*Q59p+V^KOʾ.Ԛg2 ǰ?|vݵ 7)::NAoM2,"š;p!l*xlڽBq.cfHEcfsdcdik*Gٟ+ t~/'?ll!"m%c΍wr:VI'>ۘ}:J֊}R"fAہf22",`H>GxOްkviK,[79$r0G GVvV۶n}l}i\h('X B@ìRq%Ň-fC%DX8_{'o7لJR5HLh$^9W$y+01+~p` 0ON SC?O2*N"/NTs`: }YwiS ) 9vXo/ d4'df+g?`E\\ ..SW.$EaT֝K-_ 9Q.:<l(CbRJeB^ax/AQSӄ2빘Z &" 8Qy^֩9+ ߥ2$,g,:TAW:ņzyţD2 2V1B )CbWr g%=J`l}pրH3zU, }?^'1IȔɬ[n)c|eGo{g AxW~؀쭜=CzɞRY!Êg`S\ҢiP/ ?&bDѳ14˾Z tjū 곹JWEnij[L=Lz ƹ*yS*¢=_xպ-Йw ؤiSw.W*x U'Tڬwjvꔾ%'oX/Be D)f|tUч֦b55ÃVe|AD3$'7.e$PC;P* 鷖ۋ`K7l 1ӭGnqUq>PhMڼuł x GG l'gW^)ᛌ@Z״>? ㋬qb =hޟ9B(g{P@}˴+^a*FV{TK.k$3Fo} VJapBgKH*˻){ĸ\R/P>l^6 7 3 ʶIޱlc$d/z;j/yucNq<ΙtFI]+at){S)B)3ֵˌ.#EMBR@sa 7K}k}n3v+ũ/<5M[8U׈ps)[:>BAS. OK$_^arXo4g4,oZ4P66]xY@XHܩژ@tPл]]$& %Y.V-.Za>T7{lIܘf)O Fx{l?(E ɲ$gUrr$GnD7K@@fR(5Ͽ[ ] CjZ x+ÿj\E{ҁ<qs @JyKod 3:V!/^$5%fC[>kBɟ0>:eŘ>‘D$d\Rc,+p}?%#i(ߐ=xc]&;ip5RYk[ h}56Ȧ 8{7ӀpOKӰ?_7q~1rbSNEE>az|y&~9T U;ǖ` ?.?7B,"S kbF eO uD+.eD):x6N.LYyqYbu $ȢrzC=22"@ hfyVMqec",GDk}h}rqLU̘꘾8յ͙4#MXCV.Ҟ"9y r_|4s5CpQbwl.D3̀aQ[^IzOa np9ay2l0*|-xK"ĘqDGJcS JȭuS\F7V 8oFА-F_ΰD-Ƹbr]fgj;/-l^t{} $.aĮk+J}U={7k#qc?b/W@YV`s@I|AxgF.7(5,CKbq|Wk',bl 2& QX>qFW9gI0w +_vh)l=_PzkԷ<u'uQ71{Cj7AJr(=h u'1ս)cVڨ"-=Z_s,|{:l~3; K}~MҔ+¬>x[s`6%cWy!ISsAvr"L̸(Q?uG;4^uQ .MUMz6(%BZ85|da&,"S,t=q^!'+_/5ĶзDC f1 Jɺ_xܴ%qQ=[hNXlWN`kOӋǓ?nk6'='.igP42@ 22V$M`_m;Kj ,bV}2iD0?˼7¤u@rh QyxHIpE8ߺ=_l#6pÝ`R&IEq:l[s]V'X2yCx722 a@$ @ aJā@7w7&P+H>S Ǐjw{*pm o46PX T}JD| /_+قgˢ$K;ec71ϑ 7 sKa)7F^/'LAg@G3{>8gGdW EOk͐lF=m6n/<cKSRgLwXk,Y wmg 0إN<A&9*f(s)UjՇ~9+-1 2Fk6:OjU6ۅx,_}k@,BKOӛQ6%-s q=*oU #xN+##@Hul?bG$j/`8.D))Ư_-gP~=a F)^£;7nס(J \oQ& Y%\c"g!io2>'4x_읇 ;=d[[x+!)ͤ"=+zM3qy*T> 1/zgPBS"TҁO+zkQnja{+L, 9:gѭ):XVWD qV]Q׮Ye*9PLDZì!QP/dFr?a||`m]!]5]D벏4R94mSY@m; E7q"N/u;BW`D te;uZXSMpoORцz ³,^㵕HIsww;k~c,0>ȿ!S[ʬ$$*Ch g{}S`2#!˟  0 0pG ]2Z6+j  1j1r0|6^9SOF?ۅ4e+;\`NnrCX HDT%7!x {w誆Ik#^EbD7ۻT&N³ڍRx /ml7Oo(:KX&Fr&@JkivtV๫!Ǯ;o$"lGn:[0e-폄@^\DG"O ò}XbȢzo7w~@1 #-;Ob:G}CJ^A`e$Sn8)8ލJOqLq<(})Oڀ2~ *@Yc~x,w ; AU$2@=&m;|pw7]`&'F!c]$:-+7P&hQ˷2V; cCrbf`,`.T/";,ǔ/ KF2zCHIbѸr>Y/BKRA/׋/*R.ӎ],EޱDqJfj["#T8ڌN>r8kLS’ɵU8FwIE`,`־o*o.wˋ͕:li^? i0VypG:Pa܆,} o$@|.q+;Vmu1' ɛgMԋ5Q0GqmJOdЍ3G$2J}ax;U eV{9D 3H$NpѮ%hxo&r[T0>3 bC5~T6pX^ԻV ~ ?;n eWET+̈́ m*XDT5ҤùXJkH Rt(4c7͚"k[մPd_8٘SOņxe%H?az S웖H1eeɏ;TAJ"xl Kl'npҌ/ ۥPmOr@X sB_))Ÿp_eǴVrO\ނ!ܥO29dbjH={kc725KR_*J|r>h:B9@er>BT8`hmz?nDP;`{!s)wW1W/wWTRu{X$%>Fys`XK~!j gVXx62#zJisv!3PRלu֡B4t=U {ɺp,a!;2zY,tI XHh>NύO|u @W~p;D<%Hx4?3sXpNJf|*yqˌ«G8{UkawZ== GN/lx0ͪI(Fn0T_!۹v1 ]w2ϒd%^pF̔*?-b4=6@YM_i@tX0Ʌ{ U_Q& )ĻJYPK,:^gqYgV %F*' [*6mJbeaןC&lmp)NRrwDQ s?`PtMxeE; E4muW\O7nE il]pB H$ BV?0=mTC?~qu 'Tjq:V x%,32q-LPIs=kȲ$4zjzS24yDxVy=|4W̙H>pM]ٻs k6n/4v)`*HVAƊFfw2 ᬙA#:y@X JJHsx. tTRe꓂4;{X(fՙ}֝º=HYeLf]ܡntQ 0~ JbU3mO#*H,5SK;k&;Bg)lTӐkz:|}/ԛF gy* "L?xfb,WcO+/(ߵ Q)ܫI8`Y+7rt$^o^%OHYfw/LsA퓦B<ԭ:R,v4 ԣfԧ7N s~-~ <S' oz W[efLk:Jp|= 5BY+ T=c R6 rKzzB?ѐ5#]絍i`.y"1O񯻹wsxUmRO!G?A(omh\tH{Mm?D^ke]>J+L*Rhz]ɺ:1x%H'L6R )uCuޔ0+k@kyLJ,G'!8Fr6VE3IݼCp:X<ߤfZ Q"ʍzK8WU4T@ߓ nrϑaS v^7w[J5LCo&h10^w/'C H3|amU爕zW{H@A J&c)tyCjEoQX-C0PIxT~¢j~fa壐'--NƼ767O}KV494WBsfjz==/t "&{r]"o nk9Z&z1!_>~H*ioJw cA,NoPWnFc R!+|;PR@VpLzKtcl!2hjΌMafGci#IwQ= Z{MJW%KqtH_0Ȅ'Qۋ]7zUth8}ݺ] *Tjdx ;w7%n~ÁǛvT]QƦqJVWJ*n$Je*2y2`]%tq#ꯅl~^pjߕ=U}q2j黡S4E'[/ X:n;_*nZJ*{W6epiQpl_f G'LjGY|Mb} YE !EeF5ZdmnME9/wL7w:w|y޲Z%{S۪ԿT]ҔR{\xeT݌G`Kg"i}7߲}3$ š"XkOu!NAͳZܠK+Z7yOLM`w,\Gɥi -xW`75a)MY`D9[|(s($h%22!D ?$0XByejJ]ƙƳϒ=Ǿ(oH,a*W_.ZMk>I6(ĮыgHLFK*e|a_\Uh kmAHщ( cNCfO5¥jKzlk5m[Y!dݜ.}ufN'RU~,tZUgμÜfRϓ9S <1/xbN &y2h4.8[inBN]+) əyrCWa.;IP@XN0ƣݲ5 2HTB͇uMi\4RRf(]߰*M@'uU=tp=k\cv[O+%m#ƶ1!w 1KG X 'NÜ)Mx#M@K+ɋ/Wq?eպ}c}%P&ˇ:dވOX_XņXnr]0S WMX,[AE<2 2""?@(DI "bTta8xN!yfːvRaZ8Ay+<}>B~ aI>`wEǓZtӫD;95_e:oFV+'r9/JKOfOjϻmJ쭂eŽݾɖ3uX6|^ZІcwkVkY!IWEcRrbx11䗟Լs@7Sc8|jɁmM%>}lK+?.B3D !Ey%J? {4 VZog$dvЇ-!Tej;uuu:zrw~E6 deA =N/2:O7׾9@j{?įOjzQ=0]!#"߯4Q5r-`nHivb6TUPF7r{/8d#jxUf Pg#)چv!U4t!f PS~bàD5:c/P(Y>'WLQI"`kMo`#枅sK~E@`>f\$4Zx?*N asQV풺gfq-}|2ty#,Q0J&lvc8D35/W Kiƾ~=jʊlgg59P~t!~cR6iT{+jc6abx !]O_E:g֜7=fX\r-6.U ]cVcxbIȄ|HrdڰeU޶tt. |O ZDbM9%8{HDC0g*UmȐnwSOVOGvn4;56=C$3*K;4#' !ȒQD, c0kPC⢶Us2%Qt> d3lND78m XqrC<+Ի `jD> I7Q) $׻Iơx%~dyd N}%0fB$$L&6@;uuEx6Fv/v`5qO,zOESca+gP Ż>臏h΃ 3TGI9Ȝq#7D=ibvx?ho)dk6-0>¼ W,3pzYf@B22#S?(GZG8 9^!*gowe89c`; ѨJ{!hcpXƮF_K*x3n3,!ɋBwyٽӝ#,ݱ&Cy!R2W,ċfhA ԊK 414Qɨ(y>S``/NԷ 45w&u653$ ɩݭţ42clFB2 2$D ?Pe`7[uWCeW@do7GQ}FTx^,jW9w^td~&NFZ!2GwGJr9DUǻ%ɶ:z~QSeUE tn5c%8@MɐG*aX[GW6*gU6.bYq5qQ7C̱Q@1R_({6j4Y(S?1*vCX']̫qGQ)+ fpy Z=NR7!øo6o ߅_qQz"S5\O^*Csu-mX@:9ŚU[C 9H8C~4ץƼ3vj$Nw6k <TrR7gJ!mZuOSZs}!~H4u0)ĶD|ms1J!ıTXfwg3f/qw"D}WUF@rշD-?ql&:l ֫0zo\ˊ]+ȫ}iY9HIUeqfÎ cަFl],B>?nTNnďf[KfO'l_=N+.TШ.Q՘Ay F#tȰ[CFKn^Y*N"3IŒMa\C./V'O F @/rz9 %I#20 A+{> ^dޠ z.u^i$gGЌ"I6Jqk|-7DঘR@rGL"$ )Qv2V>;𖂋S"r|9oi"pDZzMcVB:B;^s$sա&4ڽenb3XRoR8AմE6j*|Aw̅7uC}W3 a5sΞad|ڴ =mƝb*mJ|D`~t=6VbHl;f4y>=ǬEo@%mذҢg4&VQ{#B:jnG}/K3m'1GH8ԸL`˼J}rHc?$%m-h T>rY F4yO%F|ck0J7bUFaiggYX"$IQfOqf Mjo|ג9zSm&o8]K>wɚB22%"?Db) WMfc+iXvGT֔^̳|3v6h'p_>ֱs$uX[ M@ypi Vp"a`˙6_3sI-.WI= A3y|hQ2ת-d0V$WÊl^/|Awn.8ѴV{0xOq#6n#*)A$Sthb5ƚjRȻIx-Hq^Gr!cDejo~1I`-]&?X+X& ] `D12 2&S?@$`CP(0DۄHyUUԧWFjAZ s8X6<`0dѣ%=ƩO"u;㥋[ =X$l-ZxzmD Q[X$  Q\?S y^kޙ2TԣN'U :A1osbxk\gn^!?kK\ nDXRq$ 3"Wc3-͗ r)ϼ?\ TvM۪lTjJ=JmO@ se+P5{{(9Fª ^iuLPO=eMk)Z܅hKHD@רq@e{G'hj5)$rOH>Y(7.0q jC=)s ##4h6',;q=5u^P2@_igL<%k @A{(~ՂJHCH"oR^.y -6n\ep0C;\6>..q8O*[4WϜ//b,r.LCajv)%CXpOZz>=jtEqz㊧vTG'>Xj=U^<h*?gg5ޮqr]q6dEzi%7zeq{bMyZ@jnp kO_ERО`wx!Iw/"Ή_M:_'e ܐ޹Rz65^/(CXZW=N t)Qt[e{~1HL^9= 4y_1'rw{+n%,Lv $BT>dՃNY:h-[~uZg, "_p6f iZ܏ijq6K,1;` U- =ƶmVyJWDqٓLc4PKL;!O}C'lIQ3B4qd@Ͳ AV2S-=Xgz!N`1&UX2ʟVY\ij BfgܖD`&>BaDa;/l>m/ʠa;Ag[22'D ?@q`G9w(N=/BR̙+ WpS&)6H+P)TT5n M-ue}jZUo`o!xpxtNU ]:7*<SXM՛15( 8k*P,im(dSP mW^l(w;f51je UZ:W9"a#zk n{%U'&|p6jS'pϋLPʆ5~Q DEH\ ?Zڄlj FU2 2(h?@aD#A(p!@(ݛPm *N-Ǟ{ 7"- Ȃ2? =/OaEd״Jz~hiy* Bx׿@IS\`k++;dc50i59 R:vg`II؃#QhVe9Aq5-U牃@i XUELhKpes{r0˧q#{WW&FH]pZ7N;=<>iq&+a sbz0/=On҆_;QȎcV7-HMH.'2-ed.nPFK>8;/Ňw878%9dabj9l4J5'}D.0:˂^rP`@J61oOKlH8)?{ #:GBqP*lhsU*vi⿩W|J)7RYuԣEd7ŧWC@GeG~{F&X*YܘRtY ФPԑ2X S6$ :K)U=ge@oɉ KWL K[fd+64ylF KݫĢWzps/UEG4gvx]jϳtj{N- A&\5Π iqjT*17RG,G]"wiTkzkQF|"s#} eQ(CTqa n*?G4 j8RJBD-XnD[:J#se? PO%<烌(CR 0+G3CPuuwsZW}k}(䒶 %6o'sRT!͒Uvfl3A0+81N93= :$ҹ[&t]w &:l CiObi; HyӐ62|]zlQyP۠P"5V۹!J wsd[*ZXsЮC Ɵuz7%nYxPiyz-z3,pQ~O 3/Sxhܘc̻y71H=TODJÅȔo-}yO}Szr)[~EMگCbu'/g3!zz)iAB>y"d SXu,ûWN"3Zf輌ۂ-csZ-sb/: M!gZӔ 2Nܛi"P3:F|80QH¥%j;Ī?-hL&eI27&;X**ftjqyUlRZ8۫_ǀW+2'!CJ߆ G CSxxK/ 0G?ͱqwHݑ^c:-OWJ5GbIGDm1>a>uf \cYR\:Gj楕.l8l͹]:ؚ*JP BNqaEo䊑ߤsSY ¥.g-|$S0UǹuH(>DRu|uR]:L.dV5? l.IlDf{O&?7?nhvgL@OOCb޻]}`_ mHЇ^=ẑ6ȽP(6Wt]ʅjrHNN6N3Bbq(3@󚃏Fi b s #GK&CH mSR/nNd* nަæpM=@ں@5üOER]/4ue 15{wɕ,& 8dDX=1{HN"-jDi&jL/S{vNjV҇wMz}qRS.a.IZsϱ :Г*V9un-nJ&!6,X Ѧ;3p͛jZ JVVMDHܓm\[̬)̺ke $ 'P*W⪰oYCZx/;I(-W/N/CG7V6L@"\3]$uR%"Rm5 W2Jl3 U6c q` 1>35i _C8N4iV2XnY|KS}qC }}1z.J+}af#ȍtC$!_$u&`0ߟ;[:|bE(J[hA} :);D/mwl,pԎ\0r7a=8n@\r|tbJ^-߳Sܘ[C#uBp SfuڤT>Ta%WUbYYyw<gBysdۗqzEK>C4ZzD du6^7r dMD ;?~,p#  ejDBp ZT WH4ZI ,Y]^hn@>$){6l>\;ycy.w{QlPw(9BbC֡=|, 8"+YTn/L%(!"W {U>[[‘B|)}bah&MGlulhCWx0΅,1;;H,Pum(lY<άO >A-rj!V7퀦ۏ-ˊ' *զ:Xd_OcK9ZJ{(_^4:ODˀJ?6P-wO-b8-k%FSĒntH:tDt%؀OPnxi-JuN):3jيEKZ'im~ { (VW Mzk;x20 L*銰F!)5kBl menx?_ Cf|{2d+rz~d_Gp.}`f|:s\ ,p6 4C-J1V~ӮVnlM">]dpTuҜO#*K[ĠX`gI #*C  i"T@SQڅbv381GJ)M:l.]=ZA UV<"a& 8UyYӮ.㡉FoivtéKқC<`nTk@m??YA&w74dayRZ0it&G(ߨ @ {wWR4gh_e/-vY͙clެdhNbԖ|޸-/؉JcI}nLaY+$RE쾧^$h>(y1j~QRtK/ecu8#~Rч'V v_'6Ѓ}/׋E27^獔l n r[?(1*%3? cfv3~Z᭙R`YYni$r5P#"& 8he[6 zVuo,oNշL΅))?`R*<;?{:PR@f9|y'l݉C ae 00 -uUbSGvp6tx >Ա!ƌQz5G4HJ^2h`OEVE'{NVRLQyItnI"jRl(wWHd_΃ S<'~D.Q{0e-kp3ߠa]g@~{"d2kc3$`3΅N~Tpނ!1v5NyZmBQߐKu1RMM}SVZ$UNAխo9 2ls% z_^%56^u8-jr)rL]}iNǺ*U A $Qm?RC5>| IH{#(|dC^|Q^( )-=Oĸ,Vٌ^M\XkcB,$6)oRxx,W5n#nc ;̎))>k(OXS\hX~hĉqIyZs"U , ރ8Yxsl47 Nc;kgAsLuK&=(t4Ȝ6ϙQ~#~JKNL?xj)7[aJ#Ub˶x^aFzkA :סy ˴IVO) ra0rP>pٳt.Qٿ2U2w%f ͡:>4evRY9&ղ*y#rR6{?)2#[1j:hU;T;/#=qjuFd !M"T,ZbNxz=-Mqt J+l篕i@ Em1egAu>k}S *dLF DaXc1e./__kR(8;/׋[+x>[/E'$xd] p,GFagG(hM:_'y/@ TGnČ7 ؤIfחWzёPR/;0LFZż}tPI"B&G&{F w8fPUbF,7u4P6^1t=^B@(sE}CHV 3oEhYb/ŔvYb$ACו3OuT8 b{ZD0vNaDz7{)PPC/,'BQZ n@Z̝Yy}ZLk,mu%3RJ0fqf|XAo4N U.9ۘ_Ԩt鬏6U0}WJeWHKwn1"- a F61Ai#X.]IoLh߬qX!ɖ[8.eyɊ ."a$gass1*'o7^5$s$^w70_+#%](Olfvxr(Bng?KpnptI%, {VphN#fMuC$ګHid'p=BYA}?p߫5.1>ZF#$wǃc;\K|L2u&+ E[g}|R{3rB?>;غȘMOg0v8952Y0`Y 3`X^!`),r+E2 2*V@$ 0 `EGMx>*eN^ɶd&Cpl*_gы'("J4֤GdT}N[[?8_ɴ]ð PA?Vni(9=_I{#냙%\z.Y BxEAeݣ]-9Rbւ5(ʗ+m5%{vn [x4d6a ^ :f$=}_}nTG(`(oxMJhJl4>MNt%V̸1oޅ/}EB(>/rЏ`v8d2%l(O:BMe]%iيo!.p[MS~Qz1_Ӥr%b'GP|^X}HN; V$Yy%oFyڠhELmGJ“1@jjq1ׯ u.{RtsлQ`'ЛPIa^=ڙ.u=5? Hx^C 䌄aXa)ԝ'4Ptt1b$ ?:!IM5QAX_GN=lJNed^oyBk]j0:6B#TVVZzkN ("}U *j-jcJ˳3ZVV:#*H\QM7Y9[cЬpҚA-%pw$zSӻi7{8Z%@,N E{{7u~WE2( -uyH%B^ 1Gn{KL?wGn*\KDBo:M}LB-/Y;R 9!HbƆlbrW<ƵA&`5cKΚ-/RF^O43nwg3es932Q)vOʰ…C͛zmX􏾨XWYH![ Ov! Θ!Fҷ Q"kYK$:ؕjjvD;rnڹ,a.ፘ8x$7uԩsG.FW,<!K_qauûי oz'w)'潊eih1BqHA'JxFpuoi"ؒ']cId'*jA_: ~P^"oqkxם"Ë",E ~5ml9* UB|22+DaB8 (@6êD]csb=3-=r1Vͺo=qUSXy܂ Ln NaLgS|l[ꨋ9m^ۈ'3 $wa@ƺqbTǚį'o j.3r`2HӜSهt߆TDZ%G:3`B`vd'`_ m:FXf:/~$4bSS~tܧ O8^Pz}ArZJ -Dr7yY|V';iJ04M#P3m:T$ nm8oAzY=x?Bϣ\SGM+~uʤgqk?  )hy-y_$-H[Y]u*Uge݇LN!2k~2`@jψ2 }qβ~zU#bD3%7:7nu"%դ:$Cʈџyx^ƼRluB9Dݴۻx!Fu-IV$هl*XdEDŽ2S^űY8ќgޘ% vg.{kK9ܮ9 @^'@r*1D]̦``E+2 2,"Qb bj Vp[?w(1Mm^؎T'Iµp^SER^c+ lu-M3_rBYN=2t87A_,aoFB˶!̏}1/  ML^lZ(vP%&a*U̙-_9"QS6I5ר9t 3pyS-g'g;yh=k/?{#qZurQK<=0z l\o>D.Aer6qDt|lZhwL3(6nph*:qd 'y 9MU=c$ߐS3ʞoa&+]=:GTsҋuV%~_ylw|I_Y9j{b .u6]`G=[nWCa+xZK$D5G^M/`e(a+`tsj[AZ,wS}C{4RU{ x!j0Ee*ߞjvE XRhiq"$B;aZSPHj5f3S/$Zuz,=:pij{~Y"Ts%B5+ZOh3I묜ˠp9 L8H,`.zu( m6*vtυSQ.h'AP]ɼ:GF$(<42f"w Tj/tyhZyy9iQ,XP681mCk,h% _։ *~3깨"C\Q.P`뷬},jQ,dyo 3I6^$eY#͠ƺ8!fW~]mK0kw⪘д셔wF!4nB䢔W(1WŔ+yN6Eox0"񭱚UB-¤!{-.T0dP=+GY *Kk1z:^Q5p'*IN&K:2RyjsN=5#S}и"ɐ;>q9X,7l7wb}(l4\@h3}t2Ug`I9#EFPD)PUb"r#C 0-uݾIصeJ*!5|D($loV^N([O7yH^%L̊]&D`GV]yg hrtv!16 ea۽06Z hc]掓3Cnjz$E`ki9VIɤ]lZHTMAŽOײ4!"4BU22-V( ]֎K0Tr4\p/ ֓Ɓ [N6Mv)̪ć62Z3 ?ߪaWssoƺ@tԃDfod#,fA~(=x`f[w%/D~3S.K|o:UFpu "7OD1Xu q>KM 7uwb .9ߝDH&gl+rC[襯ǾTfwRj 襗L)573@PܶyzAǡvE`0x:f3h乸[05+hbKq+Q4k֯XMu,Y'dU;UFםMDgTʮg|o/K7=zXzӐFEG@׽R{+a<*D"ci ѵoF))`:T5WW+mzwg`<5YC?=> IP#ӟbBs<g 'auƦZݤ eU %ut/!V;[] Q~Cv0 G*(?rlUB&)b\j c0eJU/V DfV=R~ΟwQ&z t;)fOŠ`er(5:tڽ$bV#eםP $]dSW TtWg&Ἦے)pdn= 8R.m֤lA1D>5a4ԝSm݀,Bu:㸉ϡ8wq Tym%W4a7.fd丮L,Q3U-iei,}-W2{zߥBBvB3#~^7e<ή4~u[TPur*JJp 4H9 v)T4KT9LY4c '%þُ\y;`d~@ʹReѱ ﶖkP#V/΅G`l >/+r/*mR3Q^RͅLۋ aìWWf% F#Iˉq P5d *Ğy8Uondj9k%ԄH|e:2Uv_8!1G]Lhዕ]g!ͮU );UV8$ȋ(ܶ,s@A22/" CdGe9)ӧiugHPIk 72vNB ?} r q%@ORE$fGPt >58C!jԤ]ggiP~li0`D2 20aV@qC!@i1 }Ѥ{6&lƖ4:3T@o#lBhmtu]  ܄^Sf`r<ߩTPϵ'ԟޏw&p\uqNl8=^c1^xҬ6R8xk3N+*lEC{Je*+/2׺#Omͅr.B5`0"c1%\R~<~*P0X i ׷@ib&C O<|`3-2UA 4gG.(l2FwPj-w! F9>ΫW dpZ**p,\7 76T\Ǯd_ shěQg=47ڭ~`?3{ ـ4ys]=< z\[+yhs|~CfQ(qWxzH<ꉬK6Fm Of \SZzR"7T77 D~̼Db҈0:u Q0NrbY½#% C,v} 7 #o@>,ԚX(`Z`d~+Gئtz@ uم64UVUl aH,J(Bc@GJGy+ v?fY*CXL*$Cg/eY[~N|v%G uqCTay$M1ڥ:$5N@n$\=QڵyLBiQ\d:Kfhx[7ls91&pHbxX)U,}-ʨ3__6#5$Xӟƅ5 -ÎT?){x&^NCw̅==æf-NH@h b m߄"io%p raƻ|-+l jeZ6܄#NZPTKddm韠b|y!IS2"!)˟p1+<+HxS۽(|Z;[14nVpKnB9QUCF\r L)Z[X6\l~h3Gp#v^+RrTalZɇƀ,x)a O=-ə8'{~B;U Tc L/M5p?& 40m"Z30){\yKgmE4V[-/swP~yXn\ny-(Fk!)\HT>H-%ϧ^j*Ub?UzmSA!wt.(Mazd~<{ް R H yFBR%V @Wo{IYͫf. .!mtI "ԻEvڈ /$`8zrr "< oF ղC4a]ʢ#^US)FkYSY)6w@+ 8qjJdi5n2su|m!@7łq,c/Zno;\,GBՉf@ܛZP7ZAf/d9B:w-tdˎ`ԊtLUlxJ;9ru &KSƀ,.q: ?2"M:=P‡M%Y|[a#OPoE]'iB-nzh D~ѓ9E?m:cA* NGHo)| wD@HVi4t >;Bm!cx*(TC\ƌpM_ܞyn蕬{Tƕ}zX׋.l7FVnRr 2^H/> C`վ|fZee o%CY0_` U{v`B|md=p%|}>fe78<\e+?.<%qAQp8iF(O{?3mB[/$mRB"+i92$Jb3z6`|tm,kuQqN u!Y^ JN^vxHϚ%u<5i9=YRN4,pes֚hum`i)j Ӈ"$O!2ٲ\~EO?-? g+NgC~ .*@k0_O\ ^ kՌ6>'·b+t,6ien|+ǰ4+TF\$]Q\/x H˱iم맸YOCʨ$}|e:V2h-=Xb*ݱ@>5@f} "I_=x S3~_w .ĵyǁEـ GCpXڂe0x_䒲MVXH&+n_&g_.CژEܕz5l:? Ej6RLnY0}I+P.11'h ۺ- v 7S1@k_nSvWaw9 rg ƴa.%$m}7J'DUG=%Fc@a/WGay{c݊uҶp˴;+gqDxE!;UeQf3\&ذ ?NqҐi;yʵHږ.^u> ⯟mr'Y@+]'.]L۱>vJF`._ZAJP9#Cq 7C$P_+{!!9U\/R;Ajk?U d܈Q]Ynsa~*2eN/݅mq忿EUKc=Q& MY)[cD c(ӂ\5(~,;mȈB"|7#@Vm|9fٳ^ ߩ*fwJy St" e@H!CQA:@{0RIM=8{6\C#t޷B}"OCڧ_B`>[npqSe K4tƆRS@>y zAug=Of6 mf8dU\? o:{&wU -cC+spⶓKqZ"#@iG/6!?U,Q;ΎZַ87z>(9kKHQI*ĥ8n% 25ݬ=Ǵ3\ȨCAV59G Y !θ9yRJ^")w|fłQ[JSLpܙcߐ(C?__5DFh,P4. >8,5*cvS߈dMDֈξlɊۼ95W'YztMLvfIm9MnlDf ҵL3nIäiUdzfcLI90TB0/vU1Ļ#QTa5i^qwy4tȔ\>|N3iR3%ZE[E9K@$%>dǎh8бTDy?k =GDns}W@M9.4ʩnUl>Œ({)dk kt4Mliupl?tZt \y/oʬcrWewZ+G >@re3P<b Eq\ @ @KPv w5E}{JJN#w01"ڽX.6ٽiȥuWD3[TŰv*C/~r@mPx.Wm8/Mx?=lsbA}_DP:M*hkwna/rHڣ.Gݹ`cYcZ\CmN3jFvCޠXi8FI;I-4‚"N)0mfЧ~;HP£w޷r @&ߑ@ 3wq%”>6+!ru@z"n^+`菥,bJ>suyfP221S? W]060;drrdM'5޻̂*]&̈́7@e]O_T{Ff" D#!2rM@h&]%(5rH!tҜWP\46AKXAehxmЮx;jOGRw("q2Jtd~bԪGZFʪ/;\LV&uf&*z\s8oLR;|FXN8]snn!-|{Fb%1yH#W\DRa[C]?%5DKۮ03G=A>H(ej#uVH5q-sʃCVi:s,HtH@KiCi.K._G1 Yt&_McG;pe-6v;;/?1iSگPۇTs: W_u?cD=RS~<qD"p%O.1+F?D%222D ?@ AE(8\8bN'w]\\ć+ *H.]?&wf~Fy==/] tulfROG[n) EڼX2Ip|-_!w}]̛ChBPȮm-`h> Z Nb_bq΅5d:g>e:wKpZ_Bs xЈ{ <}fn 0-O/d@g>d/k'| X(9)*jrR{lT5x(İ(Y rT~ߑuzUӞKuwg1ٯv lc ׂC{|(`͐A+;7UiQ:/ "~EcW#>CY. sk[?Z7'Mk euI n8З sڍ n.Hf2<9GzWl:+^4͹S`g{SDH{xGGaK٫Zha!H͈"pA iprn (Y@c*42_ZKq~b;|ȐLϐw >6Z+ŕ"]JchqbCY_ 2^)Z_-cjy/Tc 8o"{ H]&h&#m-6{)=ˮ|V %U8N"( 1 Tt(W "mP{I8fu"S%e57E `?x{p@Փ5yR '%`g5~[e o,l9̌}o?zruR m PivTc*4fQ6q rMg>ְ1Rˉ'泵<E}JR7ac2k'6WE/NpTڴwNW\&߿'JK?-*N(;IUF??ElG#v&J\Mf湉ziID)HL/zbsa>myḺ]q Y!+U:SBO223"? @bpfၠ;jyϘ ; Y\-zqgӡ<0a_^ 1diA5.@׆i]ʼnI`^!5XYőϝV4Wn ċ&o`,⎩~,C dgs cݙ҇{YL@5z+xL{L?&7 Ξ4䃭<}=ϾpJRf0x6Uxy0Ey2 24S?` B SGϋQdg3I"RfCkxGBnG lLR@).YҮ^N|Fݺҗ^VIHgew>)m I>w~-6q!l4"g)G^7=u? swsW|yÅ4[FQAYToxz29J}8`F&񁙦%Lwm7o`^ve~d!p sЂ:.nab2fqnfYJPb9Z,!wp?@ĩ,(޿^gWՂg^T5Ix0̪GJ\y@?pV]n(T>A КPЂ/Э*j }*L;--Mx5cOO tl SbEdXj݃\@D_FQ"x J>XdQ/lsq:ݸ%HHgǿTߋ7j|iY,*݂M*{Cp{ǻ7M +C,8HW*[ζ Y}<$5 w|Pa A}JR366y8 #hJk9tgg_t 9ߺO"n& U(tZquffo)V7.J)c"?vP#Al~ck9+VHTZ'6/z, ܀|g3hݽhܦ\Heua8m7vYPQiϏ_`y"^J'xƧ㬗u3NQyy$Lj>o7PP٪2P-hLw/ gLbɾ{^ E2F;Tx8WR lY'Fy?!M->*9I_V%j. ܌Cb͟/>ҳ~#IDג>SV(8(V05c\46ЎgLï&&}tut!2)H(V&߲mÀSwpc‘/B 0&^ 3}Eצ OszIoC)=8*#l?0͸0 I <`|&:kMk0>´zM\<8W@d!0J8تkVݯK7m*01B9QA$6]P?kjFdXणB}226"?@$%G.3ϙ;#SI2-4r'W2[) ERLK'3ME0 ד:G 'OOVA3K vN?;_ [ (X ]B{>N-%^+?G5}\i˄Rz;#0! *>Z*h|VyQ$E4עgVᶐpb cP\o@ؔQ63UcoOӠA j ?V[ퟀnA7fNPih(k[\ۦoigFg ם R&݇~y9~ixi*F r dXo1V pۀ8%,Cؕ0cP7(VI*\WqtĸjcӖ dfJ/E}dLn \<1 /lFich5!aӒKhXn ZoWpd2Y҈^:G7-+;ۂL4!W}qs3ԹtXY91I;DHf}V1.N†{?rdIWM{>$U^@227S?$Q&ɴ _0)C+G|i'&L>8^/Â]ja2eg82/FҍIB,c~]. P*W=e"D=uz& 1DC; 228h ?@$pD"@ٕRGM9B@Hƿ\?"\i|LNЇ8 ǵ`$ȊLcS+c*XhTk5 2c1z֓k0+ 䳕r3C6x j~_AH~\m)ćA7\h/#!8yz(!eA{ڰZMA ~)ڸ ;K?;Ni/7Hި>0M#NNPP!rg !fNihKp|M8kK.\r!ڝhñEc:_Gh`o+la6Gέy{ 0jЂPDjtA/ E⟅{t:G?ڴe^ c(>%qlԥja[ 4* MMVZA<)Z_^5w;N!Dr.)Q2xP~my.e3q૛w*̈́58~9E %! 4yyGO;w=¹Pb@.x`SG\K?\R[ NT}(il:*"FU,1>sR$gDݬz7!KO%4t%6"eK|GWB0Īa'!s;'Zvnz|HE8pxl*7nUkq^aC 8Poϡ\a,pk3 _Q]nxj2>˃6.x~I0`Z#582BqLB&Wo;L>``5Bx qZVRaz;Y>sII㻀MfofEx&r_n "h(Љ44Le%#Ia#' !Qq(sL@JWJ׃m87FИk-rszrVaOb(fh D>pٱO~gT? [b>AW+PS89atVh7BU2Fn eWЉZйJ= {.(OH1 _W & pZE꾚5]=+D+qZrĆ&e#n(XYҡhrHɧj~E}5[/.+scwy ŨU؎6lqWL>0@OtE‹BZc5 Conx"f4ͬYdϟGd:CR  >s0?zcBVO@k,5w aNn0Ol&hcgh!Jdr:ȝ؀-!0΄Ǡ޲=Oq?%={!/{("u lXI3mpɑ"T ɴNzm>|m?[yhMYë2c@&T{h hf%PhĸϱYuP$ N qiy8CȌ6|$m$:vtj +S+{&7{Ԭ"BUhN D/lNݜu'Bl%5Y <()(M 8󨷧 Š84R qKx$hTۯơp[L[A@1_5@uR]W!_blqϏ^hp}B:/d]r4uNv͛f_Hwub#;%1:PH+/R;J%s vԆ?3t7Otr0 S.NAj}v Ms" WI5#'6s,Ҡ'WeQb+Uqѩ&yPFWyr<kn9* 犯'qS El?y꿁{Fh/(^FFX,67Q.J$>G UKz*$ #xOd:LJx2BO˝UW5u|dJz2"EƗK-)VϬ3136:ux$=iZV:+T1Mi`ݵq ^6)p(~(mgLFwhg|.Xc6i~gslx+Y=\9hMSx^z4^uxnw1õҽwhXpGܛ+:0 M`ZkMђU֣IJ'V){Rq:/f,pZ,M0!8"ZVvo-lf,GB"B#{֧BW\K-pw]u:8́:OD\8E}<6^ϝA^JK"˓ 6023P"єFgWt{Ig^cSx}4v0|X8 #.Һ pmO,]f7܏JZr~d~c6LWf^EO(vڍ]LDo[B[e3avQU-TU}Rx+D4fW=5,zR'H:Xnc`%9N1k`hZ(RfoxݲOZY uTI!*7{oj4-۳eռP4Z唟7'%ԮfJZu7&L]1SW~ysHXa>d.#iddQ4LEPh%j% "F=qM˱MA{,R+Ϛ:5O^2_u~`X+{De<ax+xMB.7?J_Lֶfhc;~ǭڠ E< dMڀ|gPVP +RnvZ}-/LEx &[9<~2DJ[Qz5̀ҝfr8f}BC#l7v3 QI6M0wzqpotaV&=OtyA.Wg J '֋{Ni`#;㒋>8}J5u47 N:렿Tn_vyV_Z&;壍5Kd-0Ԝgӓ&4\ʯ+v y.۹#2c.ȯj@mÎEyxipӡ>Kz51n :k}ک'D r8EkVgV|ɖ' I6i`i1j+nw3DP%I XywAQ[L"5=41)2hzEB[6)8[໤W{;VR@;/vGK@3VW t_jPA)H;yw8TXLv (R96Hr,t(229DDFA:d҅^\e ;Jlrz> F t\?OX@zMK^Vgd0ͮ D,3_i47 DއEњResO@LH⃫ xt ˤqW?vJlV%.|5,1w{Into:CoS3|PǍS-o,{rbn/3׺6(2V_/>n[deh ]ᳵLS$[ -^#Y[Pǣ05A,YJ@Ra5Y/A騸za#B]sm6 !oe{@YR嚳Taf4!NZNty9$^]z@Zu^ cjSEE DG s22:"@$D`@ @F"e9ֽ9[} RfM<,Іa5{#5[*oE2ANwV޵tL gj&mUM&i@!Fbf#n֝mWj@kK|ѥ<7:^3"1~4 ؝9RHE.ǸX[nLI-9M%ʹh^'<b=%t[q0kV J`VnT.^'T#NNu^2FD\;|* c+IRGMkMr f[o V^͜Z O|/xEd/4n]}LHt \+YaP>1dS=uQq!%@ Y n68;&|_@Y%c]vwҏ#+Z&֔Šu? р0_B`pƾ@z4b5y, & $+*/e>g7S;~mTcJN;znyI܈Fz`>D*ލN;.3mվ"#̀i>Xzfm!Ȝ {ܱ{[VRBmpG\ }ꐊy᱅ӂ ,^y" OTaa&Hs3%bPId 4\_|LA V%qR4 -xt_*4gWB]d3 h|ra܄c 3N&5Is y#ؒy&-5KO񣕟E|/B 22;V  ffNl}!'EҢ^T|y4VlЊN놉4p(k^^K5`πjV]#oov 5R)@k,NZUڡI ʹg 㥤S͉0 ~.0bixO1X$ W>=x&J$ _h5vDa ;Z:<U3#س:Ƈʯ[œe1 Sߋ%m`v955e0=\:s1r= Y^nO$|"~%j_n˵A;L GjUxgiHZ FJ؜%ҳ({EŸ1sޕ&VM}Cf@ @ӌP8X|DH7ě*YeN @t[31.+{1/kiR)-UC7x;2D 2 2<DaD0 1kf眍]tZhUddQ ;EHF 7p4 =PM3LP*f;?3P7$F9l6cpa(ݱ ` Tvv=<(:Sy k7)&JQ=j~[:75 &?e E~I>2Lgx&F=]'>`PpBܮ1_\{OS4봋{FXZi 6!\cIF^ű򁪮nzWm>͙5X£%VITn5sCrN` tz׏:>aíi䵍HUO:5v-\6<|G9`R$qKHS`huB%.K|2YSu:&/XD =YϊB:0yM+#RiԯD3nH/򸵕tW-~xoSoQC~i;59O:a+)BgA2`2 ߶ז}!J$oVVG93 S.AW?B 22="A3Jh";eKlJ-Gԡ`ɣ&+'Ԉi, ƚ\4V&z09ʾEHWWZ\ڔ71xvzE{($o$p&.֢V7={9 ĩL[D>E UAo8~Shv%*U!Zc ;6qN~ vBE:2 &wMe $W[WStRˠa(t3xw@ulS]]zgmVc Q$i~Zgzq8aBFOXAd].IIw 8M{Í*lu +#2.Yœ|#Uh\b*jl,! SEQ~oP+JF`ڵzR1 17U`RI`;8 FHgaI.^Ν<6t1xSBR@@DF 22>V@$q hF(!d`8wQ}*w-T3"1A8u 3n15ke3` ~+KO-}dlQc']hC7NbRox,;GCUпF|"~PɅgYzq?@~-wgg}4kTơao N_ ɝEllDRsl\^b S6BoAAǚ?dT-^ق ٖmh!ތDGW'N ɍ~5q-U C&)- 3 [HeAXɦMKYTҿ3sxe)&q&!P)8}yQHsmDMFCb܄s"i#$XnӉFTYڳh@z>~|o͂b# GL5a~O*Kl׹͞? QAu@TT8FT}2h5 w+Up$s>BEo)D;?d9G)*E{q h51 _SUƦl9$"d, +-S`z^Tmv!ݼ ~ ^/:K#V)VcO|[UNby@Ϥ0}-xb Z%:Bh6З`yEuVBqmkc m'۟ lBM'.D'ičzT'ikz`O-&է0b%>!QSM)QsZ'E[Z(kSi2I H]+X(ZO~]gfi)_D袗)v:6zMف gs?tG(^ީ=~δrsE̥si7?y/$UA$I!g?ۓ}W;'z7Pҝs1XxF`hTiSpkZ>?24 %-F\ _:4rɜYր2^8OPlt8Bx4^>tC%I~Rq:Q<3Vv0>AVknk (mZ8&2W̄o {S2QˉjhY7P SW8 @6 #X*)l^pZ5%'2Bq)v㸔UI:xR\PceE%uoյ{/18 mLբ>p{N=ʿN#qdoHoA)m33u2qz}Dd]2cB ɐ*?fPk;a`@YmàbBdɭU߇BQlƾ1$:k,-֭W+1ua_i_GVI$,V}y X\qLflr9 4 Wmc< XPq! ^q"Ti~(ܼ}af6]_$Ѣa{ k6Ϛ_OgO^Dp*3AQ(e,+4hӰdWKj2uMo?g m׌!ܖ4\#ackRiF*Tۜ\ ;θC=c +rOV94`5k/vNxL6 2ow@^V":1Uu)ZElmvIX|xX+Ӡ&[R.V3|Mzzʛ {r*{@y=)p9 |Oa]Y. 'B0E9A]qW̅u_|Z }[6& ` 1^ ͯZVfw yS 5UPfI~Gż#CV) ^3?RSj3z;k?qnUlRK)`2B1C{$lEsʐoK޲~Ij3IK7;?6u \Z+ǤMhoRpǂ dNwoaX>~"Nc=LZw~dVz}EɽؽP*͈n^kOp tX WǎIzZ:JTWӱ]fNFWnck[[)vk6.˞n|l8T)_ȕxfG_֍Qp|hDfw>SHVdFf!/?.*]g{K;m^V .>_O-ǠBR])e%k pp+ò ^ n` `#B1 . Ĝ{Gq?H@05[_.Gi{Egē?6`[~<ΦZ#}v=ZOClq˰tuZ~#/Y%~BH^'\ 7KPC~*kMXlrj#3OԆƜU:{ͣ&غ2t ^Qd6cgXUqOPXQz/FI$wL7 mrmkT̓>] nIi'.IX}ٲv7gnT>0 ^:կDCƘ70tCk匬1%1s5 aTfE =F7WFXï$h^8DNUPR0*MyF-^9wVom D[]6:m3$Zͻa!EM"a6$)( 9| =\GzaEaCւ0p_*z~hD] tXiSD*!i$aFN=o@Ye"tl3H1~d7*GWXաq)&,%IK`hKljCSX|X{TM^lIy3" Sџ!žy팩e).6x禚v3;14@2DaJS@$or庘U$*3Qս4FLņvkÂ6gjBՒq>!Nq6o f]@G$ 1TLS"g*6 )pIp-OeQq+/ Ү 2:&aJC:~ǽ2Y}; 5_:'xؘk> 6~Uχ) b=$wk<^[\Diu}u-zH)ǞOHPq&ŕH*J}tkxp>ںs\ toN9,c޻Q=mU%Q E϶yf_9Wn2\B9~L>Q∑FQpao!^vĜb:.};>j݅hȇMȢ^S#Gѣow {r ׬D=C4Y-+0;;nefRZB?nʠ:T2 CG]瑂rc۪|'k6z374Ȓ ҃tfsd11^c +-O)Ev;>cУּ8y,&C\P1,n740|d>q onPp[]PaQ+d̦E!U!y#~λ߳,nYHgVKTkᯮZA "O+5ĝ/,RIDssѵ/ru3}'GL:@Ci,D{r&7M׽tPnٮK)5߲>K~ %bU#PG-ӽ^>/ "1mq ^!>GޡE8aLD/mf-]6O8 <@æE94)XєW"J*gJf.αZGNy Ƶ04#~4L}py bmI^hF\fN%j*orH>W3R ?&iƛP`)y orIƞ7 7R%Ʊb\bOu n?C ()b8E"{)-e22A"?$"Z҈8f\0-&_lUH\0^W'i"ʌR\N2 CN2\G ^? o5-_ꡬ]9YjJ*]Z)Ʊf:G&mp)[ʩbdi2`H>*SiG}Gzwɵ!CEi4WU@[9[Y@̿)x>5s]@ۉ  "F5r0,'풰؉sO(6jrq07I=^ZY FMBXq{)nQ ŋ sN&88\dnK29w {۽jБ 1Yި OZׯ<<e 61?COv?{l=</g @ôSG7p>}hǤ؟}-/ђ}H9u~}+@ToOՊ[5)7bne}:drz%6B87sg1鵚fVU =]e$+2x/+`EƁ 2 2BS?@ %x E@TEZ6R0mCڊ>'_@A9epsvނSB36M&<.8/y "40Pq?@".0syC4qf`2`+^lz-2Y:2w`MDU~m6D 6WJ$,ӳR FGL d&Gχb-EC!xCj@2~NjEd+bێꗤƅjUcA7.a &9b' uKQN\" l&WL9k,߆NFq%,F$!ߺ+ w3_4:>(׈ё2&3|]%#3_IvzC?e誰|8(g7Drj|QdN쯔(N0k*$:F+Zݕf֗&K~IoFV];p4intHzd^5f-͐.UCr \=h[Odv@rkrFFs~׻ ̎CBpuo>V(k\>o60G˥9_!(O005q]tuxB=T:U? 'J ejjGO<䝵g/-(> wpg4 V0|Fo83K$ơNNltZeb%+J9 ɦ`Iڊ*q^^NLL#ۮɵH7 |s>|@C 22CD ?DA%@ %!@9!C8Kw‹o < <#m2r> $YVj|cS y8,{=tP#J[>DqVЏ>r_m0eg%_C`F T~JwLHjV};;'Ow'M4?GSR~ccõt` 0 ]2$Qh裔bҔz0=]vkiSŵ}?cq!1cB0s N,T5 n@aI6*cQ^atcׄb^bv}Vet%L9AZs&FADo`ҽ2? _+qgJ.r8!DKyMVl0G ˬ:~^Tqd "P6 L0}WtTMŎʽIL98ub%.8D:4L$@"3*# H~D|\?߳qGj+Zf mږ>>"jpQ*m:RH@Zs-(NiUҚ42%{\B9jL"_2u h|FsxsɆ{(~ Sʃ`yyU'iJ 6:u/n);mYkS nd 8ކI ?;GkKd /yL7ueSEay~qRvjWM/\+L"2sHB1]"(>jԣ)>m'eOCl30oyZe)GS/z}OT¨[<u3ʶQS=ZRG@ |RP9yz0JZn),0(l(SA5$Ţ>NEW+K(҇]s7D\s`A3aqRu'L&h[p,+voȼñ)(۰=z)P.Ob2 r >®MtbGXw ݘ8hehEZTqRF~:'eT/88i7a1n3iꁯ&BssL.gW}EZd [Us)y!^|Ic~,)kJƑcYW))Kn2Č$ ڻ#Y^kș{tly(-wqr /(f]YPǻ[(}m7>@o3Mz@v&@'APFզ$]XXunjUڦ%}x:U7˺X\,f]ts4Gc-^ ~r)4Y+?c8ǭ oQ-4`j, eB;3@ܦ$W]/lA?pD_ìj~fx &tU0NJ0hs˴ԄnVy4m/SXTأB >22ES?Cedt\pqcR ,ZljɵAAG r mc)9@o rMRn y;R Y*,Fy]츸.%#@eQy+E 3ze T IY (L0N7ū H^ R/sS⢵^Q(zr L\U̐Sc̃`C`0JFǧ}ޗ-L>Bb&-8BDwŵÎqd{HpAí&I̎.)Vz d%84шP'v^(˽ Lk48pUq(B@UuK$\r@z{Zڂ3.' aaD&!(̚%N Ĥ!cGs}' B*)> Abub'sM&֬sǁn$2ϴD mE#;ht}I_[TfBF|' =77_o~ dˢv3`Q;-~pŏFkl #ɡ\|ԧHPEGn% 4́DZw6AZxcM%Gr]}UKGS c#|Y0x~ڑ?-l895o1!rHrinpkyg-%[\%.xWڅ% XKywK70PӬj4C,KrԦD h2 2FD ?@,0 `: a@#r";ri4O )tBF(Qױb 1I.ܫ7Ab_ugߊ : \+8ya{:/Z kM4IKKӘYy#3Syvn! )O=ڟnXEȱ0@{%.FΩoQR .X{xdyWbJ\ f=o`/*b̤AHr#&5%3{K> ^T1J?zl4HPA{j9gb]M+H/3LqHqcCIK^ ZK͍J1yHJ#eE#nk'fZ{m34G :T&۩I"a^w?V}աRw^wSẁpD`jTVGU-lR `2Al&G8-HqдE-kE%M?/8dIu!êѠ 3h j!8N#ÑtJc bd$J-8ˋ>ˍ S@.Vk\dRHnG `Cօsfo 4'>q|"g }u|ͤ\u0-#^w5(i7;r *5h-h $Nui 'W"1kHq,Ի8fv{Pp+_*>E{b8#9,LPQ?f :@e:R×ؓ"<0s¸#<6}xS&.Ӥʖ#ۥQTk|؃Z"Bb}Q}CE-t䬟eqԈd Yް"׉70 )}\舥'讇U tBͶ1Yk(B;ڸ&8L/,wk I ݾTW`h*w,)RgP:~ =[ >V%ybM' Hky߸ (|qWVK^7b#؁one%`RAFZ2-M# Ltjy7VZRTG Ay+ rE:D:VўoRB 22G"? &"Y]'hάaZ(?Is3Ρ_p !`;9Nyo[JӐ'ǤUpqBX:0}d翧ʵ5drRg}zzHCӍUwc>7aĢ{n(+y?C8': SU>8T"2@ē^WB䶜.|*l #HGV>uWqawUeQq}d^~{.p^fx$P FBA4MۊH:[ dk3)+y&B^sdTcv_ewUFs3i =e&Bk6M4=^Mhkc`/QЦNRWvu3MNV,{qZLFTwGռ"~nZ{[tDF.Fv6%4] 82 /s=+K uBOl\1㒽Wb~3B$r̍ #3oף/,XɬmxWD@{6|V5e?YÇ=:n U%aD)KC0N4OtPҊzv/foljeY̡SwjF4 2 2HhS?@`((`d@[twղ([68XhBz^%!Yp8{QDk(xbpb;i?i( lr@j1Vo*x `Q{s?DDƝRb|>< ª~;wllPMN@UL֣fXۘEjwR hQFzVGNV+#F!>3 w9Z?tgۙ<ՈinzMnK/FNzU{WJbd2I++縭Q IW.x ^d!^  6t$^Z[N5ODr}2Ďl:Y`jLk~b7ϔ~/\ܪ+r5C^2|l{.l Bҋ fFV"yFT;@ɀ#Gϩ7<]u6_|/{Z9j8G`BcMDHxކsЗo65Hy,۩l#6^6ڽ~^Zpw&IgΘ(6^EK7İWc0\G4J[KoV°T bdpv;+>oOYuRq.,݃E'O1^E +R #%8DD$8&|Me|Df6e+9{# T].k`Bb{v3ڴkWK"->g&^>C3pO[BH}v\͸Ït.zszy//5YYU3.'Y2/La_4FW @XOjz^ R9`+SJ b_<,.TF(-7տX;jѿfڍeʞ1X1[zf8}&lQ}yBQM1t T?%Xy:' /hcF7ͧ5˙<gxrVbcmvH^^0Tq\ 7UURv<ҋM>`~H[+K߭(Jմ< ߥ-q/]P W&T[]cc ~vY^} 7B%ч T7@* FSnS[P91C &d#33ϾNn]{<䇃G-DH7<Xm) *;KpS[expuq6h}5BԘ$''L^3<P;?9z\#g f kz*rД|.dz+bi=G"G)=dyNT<7.yr3_,iJ!VlӔ@ u9/.%K/.\n-K t&tŔa\+<\hFW.(,:Lu^ Hc1Mq]-i {b 5if@j;Ǣ/P(w `84$쓄u_ v>:?+IbAw.kѶ;.z3)4F-C S6II"ޞs2ECP0x(USlJ-8ɤ $9JA󒼉zAO%n0yѽs:uخ+.&9tP [f*1~<.4{@mG1G렁c"i֚)&SVp溺#Σ3 '=k%q:VieNm%ɍNtl4'hHT0jD&yd !$C~;l5HDQh|CύJ1maګIݯ7VKkGO0>2='ΚHwF}7:~zyZ6 avlQu7a9*M[|¦YoQ(̣*U;nY*@Qu YHۉmѥMD-@Rz4S{aﱗl<Eh:h󎆥 hЯP4/U)9v୲͉+\* Aݴ1IɄΒf2תn{G'5%(mQu6b ,*snY))o}O^n&XHwS/In3ۺa&eU߂7:;؛?6^k=dyR;fK녛2rm$GҬv4|0ˑ+xF;)ܸ7f2:*b13f ءP*=c$=sfa$ KT֧ƾnZ.ЇqMQ`ܼdZ"+ny5YXr) 5B%wEѝdy ,u̕P>ʊh?;=K>e1tNbfW'~\('C;lU^̗^Ϩ&$$16R~p}m|ssErD>RLy {l&4'se'oTx䜃h#cɻ֍a3r>iKU؁I$OieJɡĨ?(w"fÉ lw8 I-' c2ehG$ 4W1`޻$`J \SGG|!aH@@(Q8QICZY^8v1Ŭ LxF+01~R- c~7¤V%eѱ-<od}U}"sR"o´<=:~"qj@Jv5Hϣ(6 |fXwuv{ BT!q9ʋ=J~@L uPmzѭ0]=x-ʖO$e9#2fÁTZu<+e '̑ 5p+!KH{Ox۶=n?<๬< hlO[k^(^J?'7!6Q(kW]0kْ=6di<9U.r0^۱'/=f] %!7AȏGL?GU;?v[8qӟG%\lݼVPӂAe^ H;RFN)B3:r2cS00,ʉz=җͦ2IIm52k#Rm[57ĄKfN-}bCuxuSzy)Ñp}PTkbi#Sq'hί ("6ށPt%3fJtT[X1PzRQ/eE6|?ܻCWA5a͞'Ze80WݫB-F lͷ4|`0 ocyA{b̢ݯ+mPP7y:Uw߽/$[2W$WPg!śRA##b>1J82ʽj}o^f~|dLt@/w UrP/^\Cx mΐ.~)AVBBl0v@9s*l#-,U k&R:i@;z=[N7Uw$vP<$^DEiig`+J*b9/UVbh"m&}q6S||.?<./ps5ޠ=,>hU{輍KQąh"سٞ*yG Z4sۜKtV;<2\$F[1fwD*yrt28[ Vԡ&L7;W/GJ`.{7TjWUpW{+ClnNET\z/s3l>@d?3dVY~P7' v\c[H) *gJ9f%;ʝL[cs{}b [NaysuW4e7+Pp&ﮆ_lG4j5J  $v>gIy?v'A5\`22IV jB*P @DJ{ejQuOf @eݞ.nRҙ N:g9Dܸ$`GAE;d/lɔ$]eV5o&ªNNi^s%60v잜{DKTgR?Io%RU OuD~D̘D+{7t;U2}͋7$'zv-m NjAք;gHV߅ WKU/q-opœ;zz6h&CR M[B'}L)rVN:$.,%MɸoWmv0 y5˗0M VMq8yMVdl쎷JZqm$ x zo3?؄!,5q9KGo Z1(X$ Ky 0nwWp$J ؓnO$4G ѕYԈ-SqPZfkDɧuo HHu[hʚ2_+NKXi謇vf*!jwS1iȗ)a 4pr3QłV E} 2 2JD@$pa<(3"|)zKAf٘/U#{Th8w11+PxۃPr0d9ן/~y_mie Cu,iʥ7R|dC> qqǫe!T+o}`:A4LXYE?wr3𶔠~(j7XDe+>I1-jH6*o,HhB'kO}T:"-ΐ~+|+M}ǣr p ne=ANSZs-u%{sY:IL[n:i/pwJ[wff + z3QuǼQ#mLIv]ܫ2ZE=vn #ܴJ̇DAICLi[YRhH .OۇEg6L[0/Pmzܨɚ !kܗ4%Y3=T.p$pە_:AGG[Ԑs+꽑g|`ϸcR4/Z&V78 +]a.5c) ZTr)kiN tRp?SR}~_V'ONGߔ) 1sЋ{x䰏,IwǍٳ":Uh>pX-HJ@JbmIY A,WZIO+hq*`m/햔mԩ-v 8jSыd}IjP|.ztO}L {Y`Bw5ib+L]8e=}6!&ޢԔ6spP: xEbyɾ3}N[# L9\j 3DQFRv=Y/Px-s.{cujnb*O!OrKE3^II T߻ 3>QTaq0w "cxPsvUY<ԐT,lᝪG}I2K7eiAKӵ({$op_q SQ:3D6Ff F}iD! ]6<1D4<_4?BSҡX|έ۽ȵsx%;FWbCssuYqfȞG. &zmwS(- gweXFp"57וrSx IhdnrXo :paCC旤j*p%hcV2khv+7\;?g,\c]팴I2'NDi41cu,?*'$ M$+A݊cSPetmLм}@3 Aj-SȢXɏyEF b2 2LV$A@ 0a*Hf_UN"_ ,<< ;|`Mܰ{<;[zQ3J6X[P]겂mv%h,_(*WxǬd~G_nk+D>Kg'Mn#^B֮y1&qMm.DJH1%fW3߇4j[f4V>?"$b(ӴEL0Lu"^TL;<& pZggaɈf.t'cH_UuTJWW~UD~M 82ij1Yn/5!웇6^)I~X򓢩{hҹ v|*.g'ad6pj:sU0qл@H-0I$2Y =vPtW vsd4=|k.}Xr2VqP8Dkzio@` -[/J,~plw՚`:C9vv]6]pBk 22MD ` j'`8灠jwpt!}{ДD5F,S#|[^YCÌ]b˖P4؏&=򀎖 "p>'(ɷSuLW@oq.`G*./FDU< $#!#2E+?l}x?)qTLc[%D j-Ϭ$ԳJq0YQFx!*yWllPݷxyC)cC(<ÜsN{/YR~XЄ/3YheEugZ+f d(7u?%GoNR #A p vI֫1N2oxYQ8E-g#&<0-{4~.Qȕ#v֎E?]63Moܿ'ƴg"?Zv?c, 1ɆE2}.Lx@TeoջT{Q[Ѡ'p5 V 1ο^-bf"MNqI؜hXO0W3w97]'$¨E70|x-1kHNC@\.Lye+:  ||PnJ懂z %B9)Az𺂊Ն~uֱ"-GYܞ,Ihz)9IT=lAQ73a*{8zoJ{D*O0` bRK2@>_ptqE%`/z,{Ɗ%EN~EeB1"}-q/ ~3vz~Q4rXg>U@v8 %!Z0`,]cdQ+N }|r=RLrզmǸ>]C{VU,Q|'r[|uƔķ,4;ُ1vu8 ^|(<ƀKl@wX[NWp5q)s0ʞ#͒p d}})HJhS: $]gEO *V7 mJm*s2ejU9U&xEiDZ B 22OV0`0 @3@d8Bw~Il-ɛqGgESԩ6YS׬ E%(k NP)6,w5p+^틋9Ɖ%rDNo/ ?32*3b~b aq粙NLݒ${I;M}1bQl7Nv˻5k萍fYT3PB1x[sj_$WؘϣDw5/Y1w#h)Ŗ\-$)=I)L\"bo#&%()>ȋG8WT%;U7P崧E^ l~ X1䌲ƑwMJ1_nd2tr W^/<>ZU#qc@Zwߩً~w6UCD 2 2Pa@`@ p(`&h@Պ#l.ߖjUv &.h:bm.Ȱ[ӈOT1+OŰ`pk@cNyQ8Szpz@q1メ׊zz?JG2L&sR:䑆l\9bIP5լ=#AzG)ӼVgc,ׂ0nif|/ٝCHzyfYܕO' `fXqKp gepR~q&!^`Ё\ڱ[!EQ#FG+AS𞇓X.~Jf4|i<)7ck|ˏ`Ir>! ^AohAm\"2,%1P :@be#f8 #a,ml@Ӣx~]ѯ_j審uQN8e. ?+[+3/d^pf+t<|̻i2$+[ Eug?pV2劳#{:%W :lHiv$ַoƚˬnN;'!.SV.Ĭ•:%*҅cqn{ը+Md;4|!i6])H>` 7t_y#yZăXd/c3Az_.ZpGCAʦ9o ڞw')El㊴$EMV•LBlO Iw>2᪱wTiJ6[> m^A=<O7C2 IdM8ccL+QprR0`Za|f|tWO K 2=IZ^V37wyŦE~{_`ь^C}!!%WHo4 DQ'~mI pƟoa$SS s%`ߠ9h qsO ["5o8g\QȇEya)k^K"n*KYՇDU2fVMԐڰa"֒i:jrHs#{6Ҡ`-|ݾMa=##󋔩S 22!!(˟ 30 C?A DGA _yJm P?Xq`5~"Ȩ Oq: OASgOSƠ8F|bKՀݹi~ɩuV}[#IT_>\:Ԏ݌6VgK~XU,UIKcM aWo=g_JFK14[W dئ>| ~}_\r_901?slNkd~@zwx$^%EΌ05{>8nbmX\ggIj緐\{r#GcD/rH]htW\s>Mf}8tXMOm>\/u][m>2g5/eȞMXKn%K5@9plbiT0|\!PIEIq.T!=N*v(ʯDr6 }L4Tk *ƲݔؿNԁ]$9--}bLz`kSL +Tqmk]MZ+(MMLhTLT{QTxm1To5˵,Ѭ"~HgL*H_je<; i"_#lDKJGw89kpE\eC1tw-Yu@T uQG{*һ5$R)xMNJqͰ,㛃jb݄>'"LDpw7(s3v1 7M S@ z1_%N@Elj0z!g6Zpg6F#w&66i(BtOzPpitkqN$Xesa9`4c*&y\3tېo}Ȣ~.Ej);˙HG?f@p8Rj \d~Bz^?/vSʖrmj#)C<0e AwFಓzTtIr-#a۔͎:;v,[\(-ћV1?OFSca[pބ5ւ%F N)2gKOrWz|"ymx X#aR4e!`\\pvf/@74l}_?P8Dz-Э7T$xL"7mn]P+CBʟ)r(/.),Zڰ˾iBvs0HVZ2Qh$D&t1E_C'tIՔIKBEI. JILX"*B ]m[rV?%Ѭ:ϖ4(x3Rd*W\5 B'F)z,>C&R[#9f5=+\uyjEm, /Y<‹ė聮ҨR%bBe/xJzhXg \@Qv0ט6/˓=M,ʀe̟-VI ,L)YmO3Sd /l 4$׸rrR\'3ȬY;*jd!V9QQ-RA/1~[mYD@kKi_;@x!-"o:nx; d<Z a1{$ǷESmܥΈjv<ޟ{QY ׼Q1X,bhgEq>2|>M|ҸbLv)3wOtN$|VijKESKa_Z'1>$wkaf 빜0^Ƿ;8$ ?A  !ղ6piLkQgTȡF뛦X246D̶?N# .VUpC_DSW*Cp"F='` `^1y֪i3O`g7<`$];ѽo@=ZZY-rqU=mhCGilqZti >%w=60~qpݸ;?, fʋN8Nl6w"J1Fj!Ax<`sx42O٬ lw: lN>YIP U>=Dq:IiU"^,K}SԡpC \9gHp?ڳ\KMЭm8CFŔO7Z]qF#MP+Rħ EYRl$.ׄy ]hK9. kiun;(vt|}A9^&uAjj&dLlA.1a38 -N h\Bd> ,\&kR:Ѳ - CQ"kmN>yJ'BD3"_'U e绺]O܂)v2)yur"t:ㆥnl5%Py{c>ԡ[ lHy@3v pe'SzzO4Ls֦"o/{'i4%?sGۢm/$ӎzZlfZ{*";V (Ԍھ5O׶d{کxfy~"y̻'׼ ]]"*Qs=7U:Y g %\Gp{oyؒ#xE H"Wm`%)wbs)? E1OX2Յ`֐j+$ _@®K: n$ޚUi˻ 4œwE;D R=N^̕yƏ)18͊fmOAi@+! Tz h":r0&&xGvZ\CuMgcWX)Cֆ1m9/thAَ{3cKD$o_pe7NsLr<#Q ,cDl; `7GzU^VUlE7HT2DbFR]7OU >AoS+} u 牬hY<WU:`sRM/u:$aH"GŐHaD/[V`1svKEGb.[ ($R&/3یpJۥ_Q=ݫB^EM_L'@RiK/::=P@z]"z7͌ʆI}`3EXڹWޖ0݆ۆ6렀~ <x Lx# QЇiothN=IA/ig]C~΄ݾ7{wYo@*Gν/ڗda"b#>v1lĦKGT {66tlsݜ!|L t*e4vg L$3p!,ZnAAt-@tRb?^gdhZEP~0aitB,qR`"BÁ 22SS?4qf@ @!"7nL],Y"t9V0^O:V֪<뜦ޫLmׯ8I`ںpL̷¶c+bIiKF^ BP eKE͆@ rhzxbM:ֱɨT$~EWQIIXv]b} cRMkLx7^_g;ݦmZgN `<_ZuO7xLh`e`FɍM ~XHTyGI>Iia_OMZe#|mͨ/xE5r-]qTmՑ!ƊH CޖE6'ӪOСAԖ(R*qy=~c ]Н^~3E 2 2TD ?PhA00 R0W;_a<SĹ)쀍$~Qۙ^`YJB{+* Lx!cHRԹo4 S6$6k/S@& ?Lⴭ\򇲀k{g""ٞ%#f'mѽ0Ń lv{KM%g۸.]u)o i˪m4բԢ0!G*F.F3q??>>;xIytd@w&AX-ջ?KfjE̚^YA ίg J:Q\yRc|bx/Swz0N?s/rY2SHOFDZ#R ViBĢBr,$ܪ/'*-ZPNH6|H' gfn/##lBEJSor<"1 윴4`!1X+X>j9l)ur(ɍ H=M#3(;#J4<Ǫ 2scjrGzX흦Ҹ悐!GnrAƊ]c) {TJiJpYˡ(dCԽǎ)oa dgsih\Co%vc,"1N~QX2 `x^Wh-اۋ`)sT\ٞ4K1^MNSyk$c1l%PNK9,CB^$naeGc$.fZ0C/i"4t߂A|ew>wp.f5Ǩ;xʯa#4Qɣ^ȟg"FA U/qܦ`ؤV:WzכemR@˅/Hxi4̥%Q,am | b+WN=@l*LAy6fZ}Ñf+@Ǭ)R xJi GcКcL"шim(zO@&ڐ&h(Y }L1 ֜r 6b(7cByt\_ KDy]rh A9Rh腫GBڢx-e UcT @C 22U"? B(hc(\ʶ/uI`(_p SL؜Hr.o^$/gIÒhiXd9!zHZHy“2I2E 4D7.wn>?9jчKy7^ȓ>k [Ԫ>\>-gkigUu<#?M~|\ޤ1TAZ7+_rڿPh#I҄*I0|5v(189I}Y,J$ f'sz* 퇧/<2劋 d sgL?(66Y܎1W]l>0ˣ[9aGl|ZDZ;7:=d`06.Rgg H}6i {?N԰˵Ԡ?7wPpx>^ dTD}{'ppVO0=?*P!i)@{U؞N; ZeFT\%p)<~r,;$v,lW[0tO7k=c~^l4HUgkvROt'LU,ߡ$) Y_g/.4@L'>e)R@$)d]ޔW`0?,VFe_@,.V rMo]!V>`r-+t4M|c*AH!LÉ.:Z{Vo-]VF-7J4^Lp˖7z^gaxwTX ;6YɎ^GAJ"HtnҞq+X1ZA8;%=s'7j´}vx|TP>a/m'hnKׯ:SP8s,Jh\؊.^{XwS~ Xl~f/ΏYTcʟMB-22WD ?Dq'd(=&@^|O#vD9T0Mc8^Xr"jm7z*b%#P^R ÇΚGcfҐ$wjcJ?dr,0΋W1bkQ̒/R?qP ;2 I*nZOo0*ߓ)MNag͆ťn^Ei xفWոYc B_-\mꨘ)q"#/Eݧ]?2^%KBrTB qF 4sw]jnװtֵ]x0_37l#pV/y/5rg2J#-uѪ *!)_ZnX7o.CP?I\i`v'lUVrۧCĩ_%X WTΎH/A< 728 LU^ W CE?6x`1:$-Yƴ*:>>Q9D`-&0(j_Eio.~xH!ґo5i*Y*N:> "p&;ǘŒb=8P?1uyYyl=!M*$ucT,gCFYSEV2 2Xh?@ c !@ F2@ p 2+iW#,CNx/&[O,B܅0acKMy)XJPƦ3mmSm_g11BBy>բ엤:hY) ~8](q=L)?)~u7~_*4 KI6'6h/˯9ߔEXt)rA[DScYT5ѝ,6̝)^v?!FN`d*<05 ,cQ-C+^*=&q"qe^b)YN]u,tq4Iƹ3ݯ_O75ʈZc@d̎0@mr}K i/ϾQ2flۻ.Nrao <6Rghxt6ApT+zS%?twlQTIh ܾo5*6Q1O/D1\>Ƅ?XЇ!Ap-j0-ރFII&`x)t: e&'|d&ha$#~ f׌3mKGˆGEXřq߸MCV+o( {Oo4euP1 7 @9 R #0#n VfQ13ൾ? \;X?f9vF4!ܙדUZaV(fun-(^h _wh(Ko@8yŻ9227lڛep-݅*ɋFg3bI[;(,㆚11 2pYnЪ(iT =MTnukfzػޓ 0q2^ɯ-i룢jJ͙nbt|VXw֯rg4V$B9\g [<*邕E.<^e&s&-دIVu2 =\d=ȁ۠7"Q4azvև2*|+uܝU*g;L+I= 0qW`FF$}ЌfO:HDEty>TfyWVBX.@^Q_ԭupA3 4־Bc\WQNEN fN? eo՜$~[C@?/8ԳԦLed>Sz aVުSi,mF x]TT.)MfQ9 {R.xg.p+:3WM^sym@b j 0H᧮mz-[]QeJ A XO!u$& s v0j7;柛ntv +@Gfdc,,P{ķnƁX2-w)23cMWoܶ'c.ezfRH@`RbZǥF\34:yj4Pׄ}TYW |r1j`M'kSڔ;{ߢZF+2X P`3"m:&dn/;ɯj:I_ﮏ&6t^#A-OV%yn'Z4IX<:1k(tx,{D|lBR\Ie/@ļE!Sk@QJ#~sŁi1|3*\e9ŝRiHJ<6qg`"5.D.kbyDKɇt3Y@Hw.ܶKٻFaUóXBď\ _~UJ7y#n 8|Pt$Ƒ6kl >L\I吺@RMYj9õ/;p8F*>CݠvRQU}&}EHd%ů vl cfMD#ϠҚ\;#ɯߙg)~Yh׺@ntt+̡J{8kWM>`*Bfz^\Rղ V_EbuEf lA8 7 (/3j- ;h1\/,q7BP@Q忶]r0*a2:x2zlp@ţp O=?["wNI ?k\cW ~ 9wKxI3ihZ Ա#P.$k֋rK- 5P)xw͠('QWl%IM|"zkVfh+8׿@4C I[LqupFuĨČa#$ _0Pm7{J|Ed`Wm|IjqA޺]ٻ&ju}}cݫY@dp8*$|0eL4c{ p g^L jXQ]Cyͻl-#:Omgܯyk2o!ޘ/fq#\_!$z32&a'kCر!(d0o|qYp`OAO>岹EeZE-D`vUR>%3^ި0۱9P+ڰ5/2 ڒ*A^~z[gy6I#pp\{5 rJJZzMDae*vW%-QNiGCTg 8ia"_oK 8m!}ã ս(wE`u $AHyB/Z.ZPm9ɕxlp1Mbۡ-`R={[4ʖMdjJI#!AjRVa -v߫:k j K}+ĕcN]qH/*a^[FN畦qm~FHؿnR%_zC߀2j ED>}%!Sɴٌs`/kzY.lɗB|Jp*g9d@ (E}$nBj('v¼uP)g`Ktu5fg1p `32,2k 57`Z" r~!HK]3&Zkhuj@ӧ/Gg>,9/cT-A5 z=Uf~ ؋:bg}wś׆~t?Y Ht/m8!ODG;;l"i"[~Ջã%* QeeףswK_\Z~ uB~X3[RIΕ2K o&39Df%6aHT 鮆čH=ZpS0=['5gCޑOtz]9tl z;،䍳> Oo]̜M@A3x!:i&pHd?dq rV ϏeK5܉('8 p?HfIRG"hH/R5s2mɢO1/w &%s؛$+ԕ=~n|\&@ΐ?^3?שAՎ֠iT4 oὑS)zn]2[*iaU!=2tsGq#?tn%\`/OAZsWdž_θ)8X1!2k"|V.)VV'RV1o|V=,t[Z̓{.C|_ (--8TH^b%eUغFgV:-| uLoȦ:څϖQpIҒrG09&Jh YuU(@N ހRA>ѧ-mN (qjж{ȏw4gqZrHjegbD 7?f;AWJ-o4 \rw]3s< W u(a1iFrVBBш YCfSgKt _0o8gUȂgc-KTCEr}l-GjߡCKjZI#6yq[ &_oI]c񪣡 ]hX׾?_3M5Uw~zq(*)oAW^}+JTXQkq-&7l%= z-m^LZm 4XzUK:8k RI滿@/K*00ylg93NP!{u|"dWveUQjf*畊*DkcW#eYOQ>c$ NQK B WD{&8xvkD#:]1|B&hHv䊐 ^ZƩyc$W>T {5 ' h3 7-dRm r>{}.mE$o0C6tu%LJ)0zVa3-|ǻ%6_M2-" 3MwAzc i83<0~(~B/ 1G23˒9AI9\b &k|}~BxPS" Bi22[D@Zg`@'tPcpJdH| ܚ aƿXepo=qs@^vXU)j=3ƬY^~wHxi;r-Υ$l9tHv7e|Sz F{xE ^ԔX^\.=y.,΍3դiMu:Pi+e@uw$-%" s`~$WK Ehz:hw2,Jnh+_)Kelx[4;kuǂR&1=n>tw.5%5K$ | `\I0\G?7kސPWK ' ů ЅލE2 2\" q( *`Pk*NoBL..G fyJ\ݭ/l<}zdf!d PM Ӟݺ^3w0VPl^A` pgC; %t;8x_{N70YЙ{I'ZP$Öb]#nist'yGVˬ=NFK[-g8ϩds>~s(u= Xx|ߪq%q]n؎2}{ex)Hci8G€dG5_!F9P҉tn3ZfUFwn S ^QTk b4m&W*a~3ئCw LEˤuXIzgSGCHg`AwNnՒ'?VRұM1[ޮp9zjxGW`}>17bfրMcƱFPr,ú*5T3#fV?Fp,*bxP@#SrJBLwږ }]Ոi-/lrLv3*Rg^g9_3q[31;z2|ă‘P$r]ߋˆDơY>Qbf]]`ꧺ0uޢ|Cj]h#|='R^" naC&*{kmmWM~c)Am;W2/uj٨+x ^ԟL7?T&ܱ̀{NmQJo³Goz`lj5h/Y:jڛ U#|&/nsE\3N5鴭6L7e}tbqMN5ڙy0LZ.fuUQVAV\{rg]& |t2mcՒךͩG z~$CM 0IpcBLFx -Ѳ XcNNL#wv R"$ef>WH&ypjT,q7eZ`;\&`;$6!M'Ũ^PqMd%MZPIWf+x9[좬2@ aWhhU0 8%J5Jn^c\-{:^4͑-.s2A|EeT(+sl%#,Pp9'Y(=jQk`0geKqcA=}YSRl4_B4'22]V H dSsi . mUVH7#t=)3) P8 3sxsYkB /![kٓʪ}OiQ]@bz3ĞΕi4֝4F jȲ6O:`3eІ_inW, I({c*!tL/3'Cv Zq;d}WJXR.f_BAhH55!B^ڌUPSkrFBҽ\WgEc  tl8M ?-)AT =:$?P>I748=]:/6d) QYt(R܊ee ?^CusN>?b)RMjBCPX3C힠5֞9Vl{c#9޸ÌpuCү$ tŔC;C٘GzcW~GKɐW0[E`h~01{~PV !lUx8uiVG{ڲۿkB}<ޑ]r67(h!zՈCQ22^D@ qC$@D @"@ڮhdfi&Yo>b a6 ƥ<*x2"ԥN2BE 0k"rBI71|*HVE~0l/%3%UZ$wnl$UI$"α#JȻV\)h.(ϥ}i].7Å5^Ç$1)}.riyɇ;2{> NgtY5*UgXn;;6bP2׭rLI,y|z,w`}DW-TB7kDTcP^*|T ib rCG7^2Y#+TH݃ >55@ݣ f1&AOw#FI(:u-^pǏ: Z@:/T >l+6(p92}0J5)0whΑ jj&ȹP-Si)u5`Z_YXPH_r [Wqȗw*SSxnSk2БQ? PxJ)7[*3UMw'|-%9d "=# !_3%| Ȼ\vK^FC[0,H_ur$O^l;T=: -eZNiZJwGBMcY (KLּ>wvTdeݘw?.)`O`0RW;FyJ1a:rGJ'>\᝹T }VXy*Ppx ҉a9e-%;3c`wy#;TA*vaW]:^צƟ}/Ek6]!4s*JDGB,4O늓aG]JvA))U/#+I=({5ݫ[[%%ʂJrJ@5ݺogl=䬉jK9de}Z`[|%;;'ÂOgD0 [ "]PI~*K5R<8;ejFBvt@'"}>=4۱M0U <WR~l" ut^h ' ,vԚ̩ŞH~;J(-4bсpjr]ev㇊ lpY rE8 j3V!T7aZ/bp!.ηonDPOLYi˟Q4L+rGaֽ]6( -A`<[%eKr vي0rqgt؇UG%sQjwvb F`|2X^' DXPrHCI-P,?^l BqeRPW㳵8尣" 3#T2%!0)˟ d 0yB %r8\-Bu?ԡp/W:;/20nu'>/x?L υ"y dE[u˪h-eO2۝8$/p"'ؐq:y,O\x.*}K~H@(!+T j,$u97⨋BE ($UC^xjIoVa{ZEJ-o<]/Ե1 lĢ]̄uX#QQ҅|y`f6&0_Ie/ۻ [Fy0+{U,}F̜z '>*` v/ʍc0'WufPV<}<h)L[=ك(Gk6T2Od {V9DښD6hzut!᪻m h*Lj03eA0=G`j=f3x}Ks5HJĢdB:Y:z٪ s cKDcdyf}۪J>Mar zc#-ŵ(Jx{*|r/=]Rpc}ߛ /=cVbhxhs\Mm<*z}dT!7@M[]Et{OH+qnȁC͊r\r5"a-ˀe,: i^Zm};yJ\u-/d7JVj3Zʽh67/n6Pܸ~\JlOCfGPik3̞ mEΡxuߘTTWNw!)O=y0_C[l@:4_u&!& x[/".>ID }7I9Y; +/Y,iځB2aYe0 jQ7O7BU:m/;iug<,_sdgWw+b`R!eʺ&3h-Yh扭XI"'^}ssQ4Dޓt$; )k(7jQa=?73d4[0U,>@CѾ8A%wiZ2w,KB 6jO9T(>^qۧ/Yߛ`X! ~߹6%U$Ct0|F.d=S3jT#mIw $&o]o>=T m8Zjj(Dm)rw8J4W-{ϮyEgYC% <+i"LŚ:J#SAGW];eZo}eU[D/+,BNfv#X<;.X ƶ6ҁj;8#BDo(*T+1)q _?ծh5cuU"bOq1e*p(*, WLIZ[/o׋E4"b 35=إ[1%f^;RiU[,LMT6YNa~󡗼Se;,FvdIMïyƱ@P͆Ћ|4L:QW80ShIi#9jWM=_jANոIi*H7 PP7raj눢)cZoaLBenAx@ka2 녅A5K%F2`[ ]`Z(,es&KlHy-wHTk2267hE.ل"1 \ttoy 6,OR?} fԯ/_ 07 J=z~@ jmr?͆Qv>T@P44l%h㶼: $Irt`q*Ig'K!ؓ[KwzsDs[!LWY&G,ëZyzrC04nlMLD}UӮYDF<)צLCMD-Sa/vc S6P/:7L*nXi l7{N2#Te$P. ~^[ݘ2*SWEARlP%z ޟ/jl]Ah:`GCt#enyhU!’a15ؾ}V>%r׌ħ,VPC>A1{侧 ~e,hCS$y{И,uYӢ8Т]ѿgZt1%~OR'0 F]c - [ E+'2BM{,_̮$; eb&,]Q[ !- 6vAdN29t!(/ 8m%Tx&~,jʂ}nostn7̋&>"MJ qJ]&1ႏT%SNU]brQ"A a5a>h̞['bPѡk" tsh_3?VUXd/8:YXe[&΋"_\dL</(~@{D;q&G)I ]٧w69ݮ5j N2$AGI'T8C&_z~^#>=#}For(}PK,:mqA R{bƽ.-r} ~3l <`ҕ EhZOHC$ԙ!`vM`-]qGڏ0KG_0@BzRڣW,Vn"AIb> >z'RGà8r(Nq&aS0@@3l#n&Ny$l~4Ǩ=='F@I"^F\n/Qڗ'>=3|z VD}՞-9f*jI*]Ӕ&!Cs WIg*/t{380Dn 4JO]Ȼ#ٹЧ{@ϵ^s~U6Ǥ]bx%=hˤ]Weg'xN~:Ж00<-FT}/,љqס0s:t~\s#n" sRq\KVɔCL*(e">E 1k ش擣qͿ'"OihS/+\OIE~mlVş|2'iESoc_L7pM%9^GPzIRzuOv՚ TC?$@ZN sF;}?z=cuͤj>5YAƠn6oikL>;AZ`["#ziqRl@IaX;lgp5{im~"p5P|"үNzS{e4_a6lHXi+zf*ܔPyӿ@_FUd;eϴpP9gBwK[|;HDlSohr!Foiu7)I͌Ufҥx↻rbE*HKe}x\,ɛd̙4HV-lOQ8 l~F ~%v=OrgFzg԰hעXkR .[A8Rg%U{5]P\~礎 /[ֆj*@cFhorV|S.;jƽ6yn*22aS?$q@6c4Ѿw$yʨnI+]'i+>(gO0aF̤%Ȃ* \[=z(B` ."  Ms/B:6*?ή )#[yݑD4EPw[9A{9yRE$iDT],wAܗ"#W@rK27X٧a%81R~,#d}%ֱl,w,r*b$+̷!8ik+ψ6[ՄJd' 餟hs63"☄N*o<W5"Fe 鶼cnbȀ*P'|}D 0`jBΰVuS^>){*?G~_^Jv*XH7WB~6LKǠyRUᴩ]'TY^݉!Hښ*řųF ՠnh嘱RG aWyaa~p4~,E h7 '*>!T 3o:Ntx (hƐ5VEOZS]ǁ6 #V 1ʞեiӡ@n}Z@MGԘ/6,O]?Y._[tQK׺|Xcw?!U q.DfWT_|i|(.ɃNdkJ, |Ԇ!d s`ʹSrn -cssjO&(},Dfc!G{_BWhOs2[EŹ^(;H.@lpplUI(Pܻm6hgg'I2駻/sGudB?6+C$&Km y'gChxj9% n9̒bPlTig `p1\aA:R *'fxY'3h}wڹNR 4,h{ 0}\;` /E:cگePmDJUjd`_ҽ@D4{dGBy9dzẇGlOHMT |zrAw]%{pטZѧ pϐphr KL{KLO0jGje53VW!*pVl8av;{G34Hx9^umZjI\xI!9Ƃ9)Kq_[3 vpd J%'@.Bf6X(XIb3l1\dW5ƋO&ܯ0>iNGq'W]k0Wz@ņ1}Ouk- D/&0w/|g'޸Fe~}.f/-ҥ> 䮓 Tⶤ{)mK/{0kh;?g$-āl/eL A{)Z? ^EK2 2dS? `% #=@W-;*hott]A鋻!~S! Rf?>]pP=*iM4fI% 38ㅊ;Y\ , X/N $S~BAEТX~. r=tffF1:cV[g@˯#_b.Se\6g%^pm'DKYULJ+p&Ņ" + *#Y* =|F0)KP x0J1PEuS` ̓ƗבvrƌR)s\t=+[#aB}q]lZq8ci 6j4s -BU;c"&GжLdȌgk)`jp~Xcf@ Md1+6.i{%fX0W 5IA"]+1l`y;ήAE(7$Gs5AOOǘjjj%#arpWG[p§cA.G&jvrDyN58"R04/w|96m$3XxܘV.^QP<=8VčC7:+ Ewd}Tz*4tƟ:hb@Ћ3ɸ7k MfIAn@hR5k^a\s'2!<]oɇ//+Œq"8_DI2Z#bF\ka7KŏҫB]X>tF|\nyV,+j; k_֝bquy)1`dXM"zqK 8GS @a 2|#s¯5G0fU/BX)5|ڟr%݈5R6?b8(.{c0=逯)r( OrX;hZ@GG\?6Z!\8_L<3a]-j4z7oS-Wf|]o] >Z*AaC^k-ۯ+vEZ׮B ZsBALKmp`U8|f"S:7[-j/BT9#E|P5+\j;n)-IJ_얙h.ZNȎl=PV5T^Jf)pݨ?iEC]` Bfh%lg65ɕ8Q.DPnY8ªo&¼GprdV?4 xG]0o{HӄF&4oX*ZI4ŵe|A:Tx MP@= w0osxDwu:?Cu22eD ?(p``7dD@Wa ps̡b Z=` c:aM׆e,'BCWn˜j+ӥDD@=/o#$k [GF:`]ODHu$&DPEc J,RZ3իéB(Uز'O#^\qjݬUtuEnz XSNX&c9r`+WnОUv3`q̸ljEMŅQ4okI6m:n`D锏ܣ,Qzo4L8p(GEmz82o@eШ":y$&b =:!_U%o`WLI# lp,y#$xulװCNҫ̗ޚ_uV6a?I:/erANn_k{\sHE-RvG`H@(0HN`zۚx/ b b[ Xy]8`,ֲBHqdK|O3S@Vu5j]Uajb> w6yKV9Y`K-Y"0 XY}9"lH7yՋ*Gg6qUSO܉#vZC ^e)d~~ǟD4dY r;:Nb=T`LyNkqTB E2 2f"?@$rb= 7EyLA>-ȁguń^yR xpBQpo5V{{$+͏t^Jozi3EMtw0NhF b=} )U~,ULw9[eX?IB%=ӕWm?\xxWA웍KDKuPp_ tPJةpmƌGПόp5aO*X)vPH-z{OHP!#5-P*bAi7.Ҽ`,zh#_X7;IB~g.aA 2Dž:U_9 6Y_{[u,t`lb1=67aKpBnܟJfkVl92iFHME; Y}v6k)T>RBA>w)bٔ3% 1g!<ȇ7(K+dᄋGof& O{uf{Ȏ],YW3W T*|yӽJdhhɪqJ>"&>\c,ф|'2#:o )&;l6w% 3x}.n%`+*~I WF\X{qur)?}< 5?.߶uOǽO3 6w7)CdʙvY`f.p_]ÌRIH#uCr99kMd Jg&(0kN< J4diSME**r韖TD\-_Θna+u?zYUرƁs+<WpB##!h(eUBRC-Y5vD2j;jWQB j3(K{(0=EMzD YXOD`(U \ȗ[]ZKSaoMSϵi߃>5 1NDmAYXrjn> .}biIXx4wO FHވO>%fYgPBְb^#GkA,l~WRb>eo˗F̐|+rW\[(^mHF<~oP,A\;OOA![ZԻBw(w\njsZM o)WQ? 1ܮȍq"=F_AA )6D9[%uDDX]&C@22gS?0E pbXa@l~d[Av Uk[9Y-x>AU ,LwM+0oOdD+jq&jbLf K7`w_UU0@a>ds/,#u۵O?nyڟoU>E#g|4Ien]qȸLƯLZ8p)Yi:̜YgXSy2=\GQ͖15 lH|db|juȄYTUkZIid;Чɷw + \l{Jm+l>8CkxO\}/6ȆT+અGJY܈zEZvSX"ˏqUIthW)\W<{5 R]`zo 4JDq1 %-3 rcɦ1XLf#QߠOaRWXh\tO8b7c3VGaa"0[;Ӫm(_lT OvO/Nuƙ|ՈJ*brVRC^Ph&>ٽ:*}ЫqwQTSfCDz&zF%hfEb,*BD9މ<{T Rd3ڿ h$ua]Y[5"bdk䱶{Om9CBOoĬ$.1&n RDfx yɰe I;` |$Γ`FD5 XCE2 2hh ?@(qC0C(`P @Зbg@ݰ1VZ*IE%qpͿ3gq#AC 6BTvRˊ*e#p̙#G<O0}ͮϙG,gk~j#i(jY?.+}u` SkWV0)^)ʐVV`u"`61` mB_.n%_ IY.{1ayfAcdsK\]73ƃr_%b%uZV.N ]ma]O=g^SU֎[C ؄ 6>FU$5N3lW ֠}*6{ XqZf6 N58SEwj+zQ*98o/\ v C5zuIIzk2I6QO`iלW*oZ]gA}~q.c}[?t Z_Pw=_ȨvId^Y$SFdS}҂?)Y)tПI'A(Wup_c;\ʎ_'tƍR3(Ri͟Z ip3T"g6B3tu{&c=l1?8H?+ʷvqT㮻R2r]]?bliDAbHwZxay _ nFY [ hHg0/7H2W:JN/_[8"lhDXs@s:x)}2_]@ypEXNdS^L`#*vra]{nef{o |q'8j)?Oqn/?Lgoj̰u6_\ - Ј+҉ŗIJѾo>#@BKquOr)u oÇQ[͠s꼝KgZ¼B HїeHuJcv$7Zޝ(Q 6E@j!=צvn?'_9ӿo?"utNڑ-ɇWf%NjQ꣼>$?:0.9W!~Mj:՜Zg͠PX1%J1;tqd5_p^IIBݭ:l^ϰ״/dV2%!4J߆ 9p TdSk' CX񔯂,F4F OXi\.`,܇-0އh=KT}uqHšǤgyEFNq&IH ?iTח4sWr!GE_DT9͉C?D(GP3@qf}洂{fUG5izg3}ySxCH-ՒaWgDB;TlDd6!\  z~^(S3kh3H,nIUxNS޸L&;0v#$`2^ cWJ w-hTU_1pKuȈ1M;{$t5`p9||YbC~caDAZ*)a+|pBE 7p^.|PS7er7y!e-Ϲq }ИHK)"ڂT#N@}nroVICZPY|C|u o w*䥖wv@Bt˛h;!9j>tNzeα߂+b@{O ڮ":*FAXP.U~(od 5`Sޜ0b8p{ Z<t-` XR!A8=zCkXDOC2M͆b\U lf);I\v>D]_ԘjK ~hX}# )cXGe4QϨ?_*o&JiqKry V͡o$u'3kuP H/#]*έ+xY0to~7V ´*@GI}TYC V{ &1՘מ c{!}-ëvN@! ltŤI٘@4;VPiSO1s9I5(B}MB){TG+o̍-Z _ .P3si[7׋D"mćՙlVxy\z~/&az?e.9W5K|? s)fNz )~tN}]ˋZko<R&iWg(ingQb2ˠ7Cc>rsBMKk:W#"fK=81nGM`, פL:)H925H?Y1 9>JصJ@n:)SīOsH,:^Kن3e1j2%YB(;/xz;jCԨD" [SL3FDLm_͏"WidSx GzZ](;,V_eۣUu;[ 4bӈXL,荼疨&$=o ulDPg%m6xi%Jߤ#:SL'qsR٠ዸA.nL%H|.7p+-aa+흥@>x he{ ˥ bP7;w(6N\RZ^{abKv SqM=BQtk/ %E?Z Lfd$B$m}{"uc*a.@Q"NfOY,F]V5"_{]1DC0. ]P,4 [U[W(F9}a^9,z9 }¸7Hj:aP6|y"M9ta DQ$\@N>3d:J=]n`r4\z&&-|VKؓI%a_M(g)mRwd#^c}AlTqO4}h4U]+q>ƽi@nuulcщ"ſ-uVyF;\kgܩTdK[fz4n JW(Npsh^&9XM#u=;ϵ­͊jp[f}i/.dUJ[BWW|` 6aÎ/ٚz˜ۆ!ylC>*v0iO8[2G@otꘜ-p̋3RZ -Y[MQ?}CdIG`6L~%yITN4l8NJڪ 6R4>UDacl-M}2Xe:> HRWUcd"fLf!q[F6sM ^$9n}O0x$%>3T1 yjVKrin۪B)a/ש)z>M#F":ݮys^q FB>—[GTݲKjp͇]L~YR x`$5ut,~!G<- r66* u8͛On@8ke9z;,#c>gaq3`>4m+z+:XJB u 0XѿZO|9>9=9s`$ŪֈJ`kmIXk.ϐo+Wcj:% =Y  ܈DDvcvس_:MI  9ڞ*[>hzҮeF@̜|S4ŭaK\eJ#ɗ_$GJ-6*N  "aIͿ}":9̷tFn z1>-,}j!"7}LHUŭ4O;ƫ`YC3A e1!k(UaRCg'2#'Ow Z %w{xb ?dc{j4s%ˤ/!mɜr nlj8`4 Y_CMQIl$_){ԍT34ݘw΂ Qk4vo'AS~"*c0!c qG+ "Ϡ' ͯW(HP:LGWq5RxX*mJB?; >S:"SHSd§1\$%g:ao,?ٷOCns&0lґ*t;CLܳc/]z'ъP-[ ^DX!Gpv`BhZae0:Er1z?<)MeE5ʮb4s&d,;(n~}L? 'bńѾb~n1ܩ-2Ivb5.58{ }BjgMz:58{M?s" qg*~9tx|){*PPh_7q_ Vr[r]d [PgētDQ۠[䚐Kh71yE{\T+%?@fQhp*/xBVG5AΜr oK{`>VQf䦬22iD( @@bX4! 0@g:{}jO{ AˣV#MvXkNI=6V֬]*aiTncM!<$IO5$T=3QQ) i덥tBW t-V5\[*LT w~$fxx- /Ч!}`u]3/c -8όno|Rx8y BGeMak'H>Ϩ$bAE7M5|G}onj xZ4'ٟ(n_1W)/"6 nJ"$L,1L f} NJpe =yyhG} >Rj=xbt{<5`@p<_ɉ ѧ\G12|lQiQohŤ6/!Ձ]x 0\t$uDI,b\;Ûh=)T.c^=gk)HH]usCbʣ8ƒђޡy\ qMEvk %P?HFQTVn(19VL8_4麯LSxf bM\(FwE2 2j"@(qhP58`@:[u) 듬+A ]c5swsms Ns"s@ݕ4[ir}Cm0ϞQUq]36/~z=Màu#7cICOxHU};U\̾s.yHDצ @j31eWŌQ^1W0snnOϰK۟+ _`_: ; Ke 2:ivqp ZZdFޫ`}͋"a |UxmEp/YvE{?Mڢa:kχC1_2ݝ)p6_r yn`hk55xI).4k$/3|?'lLVtaGj-Vdoc G&ߎPwG34~!?8|{ew@ߝۥnS:bIkHk J9 hr`@[X"$+EK?X [[a  O]$=$l[x xmRc miT=(XB*H"2uq jR Ӷs_~&+&~ &ŖB^ڃ;\?Q) \{ΐދ'ABRWKt}+;chb05HP n@}QަPx=!DI!r HpU7QW.H>Y4QL|mo#ץ6Aۦ~ZpAG3f_*WC `%湯IY/uM\6(^nԽ>*l gc#=qCwg;>. &;,Ip{CF==[Wi_P~QxV.f^^+OW) GOg=dҎ_fhZ:w >c^wc ȼ!P.m65"q侟JCuo22kV@h@b `4#J5ѵ򭒧DŽ;yUFtx9oK 4xk>wRdЇ 9lYgˉmE&`I io/F}GJY̪RlHw@ZqORh8wݔR|%WI69bD!F}׏85`E4qBF_72(6T%fکU5$ȩg[#Y@#yX6_]!#gofk-BeRGZc(:/v9ޓT= N90Ęzȍ/їE@=cѽb _@4"(BǮ'GDۺ|T\ _vzR^HPa3UvB"*s)_JCA˳/ț4hzK4ӑ9'#+)=&j9'gw(gl-9tY,.e!yuqpk 4n(VX=kKg:\r I!&ߺ!BJ[fz(3^7nB]R7Apk?}؎ǏSj +.nQd<&1D94Jt[5G py}L$* x{⃠+Oq"w=kL'dJլv &\(Qlrp}f_BG22lD`tdH("erO.݅44~O †"i$Hc3m3#L;%p&aبrtUH-M,<<*0UQUL2? qCDa&X0iBSNB4C-6"OQܡ;'"`rs=$?GU5=~r)нp:Z&H?65|o,!!ECPy:nJg/֕ѶP9-ۭͶyDdS"ŗ[N_zWnmM :5oGVH+CVЄ8ψ9jaX q lƚCNNpGQ6zB" <Īy>r@ Xeӕ '"h]1bzBATtOD,PWP~DKֵ|:L4i}ycU(nw@-6ثNciŰ\ږwu p(]jzihKN¿vf}#RN%ގJ|l0dDy^Z v ډjc K :#gZ'Ԡ phB%Yj>$-WVR:S!.\ߏY଱^ .@x=/`$k-5];ٛ\St"J@BNx3]ǎMpF?a2pA7 9R KxINpPL#篡+-J/).f$;n[jJx@0D+6lzCUa -/܏Oo'Ye:LBw!L߈X=Ŧ'iuoq\Hժ}B&]Ӕ3U .)YiMw!Vꭎ%q.Q$K0An+V4Z:ڣKwY@>)P9RC22m" dcF?zwa)J(j"y!ήG K~/ubxzD*د6ĆA*/ F[`.u*^kz{C"'Y7V"{3\[in2/ȸzᆗT}7kѠwr_g s/"j?UC,,7KjOi(Db`A[r7Vq -"a^2NPH$ŽnjY噱E|h֝_Ջ<#9q'G zM,8۲Ve8/R(2q؞S% :TI~RXGn YDCpLJD[ȉӊt n@ `y_/f9"Ki=Eo_YNnq-.)8Uj::O^eqa^$6VCHykPi1mHPnL7uIMXinT,o-2 D^7r9 F+һ"1N;3*޹#n}K#@/f3𒇞3]SڷF8.ݗ8( mڔLN6`W]8)o?bpZa[qA  ?n(mN?/c0Ĝ*nZwuw|-#Sw_uY!Xm١ a7j,,eVvUkԾq W=,Nck˓Mi bRgbܩb^d-g&J*"\oH4x f@YBI͙7U,;4* y᠄q EƁ2 2nV@(qh(c@@@-.S#' ( ag՛s?y7t YdWNr~};co§b*_ :>MCPJgo4~j!ACAj5rϴtب{Nwɰs2\/7gmQ7Ӱ6hA<+D+k=Ѭ]}T Ye^~Oo3cnFh-a'PlûV,Ә∉p/F`O_׮JS&C}U=K @or~[{}?6?lm܍JpϱYՖ ^-n/q50c!uNA x$ȇ`ka@2 | ԓ^E$FJ2[ݚP  )ksw^:ebm0p6`j/~J@bK٦hb]Yښ96sG-cɅRӜX3 }Bti6hިeW[c^浧U.qSJYơ#e*S|OdqkrR @:cN݋sMkJYVT "^H > [ߵY9龿^ ۔ZOqIa4?̽3OLlWˣ= Ii/qWy M ΅".y`G>Q 6anj_TZ?h`n$=IZmY3,])R ЛͪZjM̦ind*Η1EL,\fdVzqKWNZSHآ LiEvŷuB+x@V&s:A DWc-pM0t:)EJ{58 a)`%"S]#ԆЄg)@v~; `mv{6A)=bMZca_v<d(*m ϟVG{М<繺J8y=|W=OAV~WєAgBMq6)x;4R|'z/avniz+'ܠ-(XJx8zLHk 6R$ T⬳V~کgz&mڽEAd:S[@z1ļg$jBʷ'A#>CQ22oD FaBH1c@rB1ڨeߒ 1-W!qUg c+en[CpkB#ˤ܏|vOQ%Hl[TКTgo*5pO8Nv6)H: Xn߹tjz>os xxhrI1 vN9=:yCʨ'RlF""YOϪxabsp@[J ެf45߮FJ&wk@E3Ikhgʐ5޻#[9!P>J%$"ncEW{7*Hwŭ| ɮ|%l6.vײQm\H?r-jΚpbZv?-MB᠂,‹ f|uۏ[]CFғVn LL; lHVHGl O[1 ),ê S^7>EbbQN R,H9eO|-?%889!hd :?LoGk6 2VutL?X&Ǹ;H)4h0uV:'+w1uB hMyn*cvGֽ5IFl+P -SM4b饊ϐ&%k#ȥJX؂8QLTUu6jmfa5*iW5 * I5#N_GI/TG> ÇRiRꌥÅm{1"DŽ_Ytq>B+shtb`63@RNZKqF'7mGm_k;%>irZ2<ѹw>IX$3Nq9{HOe/r9YvPsלw֥ަA) #}߿Ĺ\.3ў tvej!'D'u3!d6ɖ4bfBmh g$r˱;QmOxq< !#@b#l;}l0f3v& |%1i8( }|FoMߛךAj+z/Bp~ṃ\@u˅m]?Qp YXy.I4hTbİIXzؗ}xUcHyd1C!L\+'DbNkCDpO5O'P_Fcqtq`f w l6e՚!ۿCHgVC>kH!ӥhw) `|lgnxq/(Z 6B*;5>Qbg[`km;zMNj[MC#hQe=x1R,ռHA$ƏQu#2o(r$H(T(,45BoAkqGغ/m0ѓ$n.fļ,зgEa{uY`V[U>L`-L fO6aυ' / 2O=ѽ[x-Cpo@BTw~~ EԦϑ~\ 'mnq%cp6ux7Oh%_nS5ŀAr"+ђ@ !XPiJ:tMDzB"U!f|0@8Am!RZUF\{ 20yL4VL>7I})#a#~Q{& F ~:xi *5d h3C~gAlGlO1-|UpB [sJ,x 5=T3]@ NSkp'K_g<>geR X 3U:X+ J5d<%%cp n_ rd(-G@MO +lBP:@Aϋ[KO" -9 bcx yZ7|G]Oדk/xhz7T`2ҼH,?&b3z"S0׺{?!ή ezSJaEBbAČ'Yp"H>'nן$p8b_x8گ n ;+ I$qH955H]o:H, n滿ȈXQPEXꇓP &"x&OEi1Y sgCkTv!U(x2>ڄځ?  苿 k0IS֓4 џW:e<Җtk@j[ `HĄ3XiKZ\v:^{61ʅp`/whSe!ϲk̋}E3T&zF,LQzia٥ unzeD|=vKY0{aGEm$TWP3M+WPs ?#' 86Tnﳱ-ۡHpX)^]>NLw%eRI qNBE)Q>M Zgpm`g}eŃqtcsh%聁;2RvPeȸ?Wƨp$p )yUj"@@6N(C{y`Y-oD\JiZ`Fckq'T8P6_$t!gh ^h O,)M#B4t pAn=,i" mLRsٺ `$R{ <]r|io O; ,4hVP o&O~]ILMfaF̡зA|Ňf+fzC~:?==u@ReRu ~PھT(ϐśm3E^-tbDSF>&Ț[%ao˛)OOLi#f]ePB]B!R ペ~T )G'dY{!oi_ /5\p Ag jQ);{=QY3Do@Bૹ3} X7iEs脇DxtNO:͑$_B*_*IwL6͎Nwy3Oit٫v0LP5x,$TS…4ߌ32qwA#\ܲo.#d# *o n"!t!$EU7Ta)-bZ-Kdv׺iU*H"\3iù}BV|dtDZ5i7Zv+S Ev+XgBlr Hv>6֬b?FVf܇᢬f,_7t5ҷ'x#G7A6a/sq B3n} 7gr+zj6i@W%៛3`5J;$Ԧ@]5/\s}xRM fsh1o.m0Sv3Zɮ/ he6eLeVrtOoPp'W8򞶅*5qV !2;2/ ~;0Mt8@g))Oio7L8r2rfSC!y#U񟂄"K´( Ͳ(_/aacCwoPTYn ѩhW\})IhZUN&jݤ4WqfM$i}^he| `f 'sri2=c&>ڃ[oLtkM}Щn2ir1<4*)t@ls|dɘFַ`Җw g/sZOGrFr g꒍2>+]M#b걠" *eNR:u!ٺ([z$-ͱWhH{qG h22q"? C@`YZpN@P)7]!xɊNj{ ZwuoS}zEhk{ꥇ=DԸ|zK;{3qkE۾ MR潀,@T 2"*3hh^t 8$-EH]8>4юO6;Uy1"^Gn!ډk+l ŵee]9vQCcLlI6ʫ1 &C5za źO 1 nc[HKй Eωuo^D ~ }vf>̖!;ᬡDM5L46b^ 8? ,Y3SP~\e,P^0H}뇓ϊ+(jk*Tp9і'b} 1^ZYn|N}Wџ܁u(b9 VMG=jŜhp_8CNm^ b ;N9"FqJa)jNj UPM Exz?uD =Y/C>RjAȪv-p VƬ+}mF؏άe:Q$-~3t^ h7_2W v@j<ͧ4P4wu^8x`&়H(*L>Ѝj B()(ߵD5L%xiɅ]zU6!t"0.ذۣĎ Tg*ԈQ_B [V7GDHyB?k C#&yb0`g3Q x^&G'D]zRkih/.@ԀF4?0U95y0tk- ]_ccRMÉ"^.kؘbNj'aqt1.S;7\YDۊ!rJRxJhwR=%LWT<Ӳl)Dq df{ÁalɯnvKI3!-N'r$] tH7i@fRVUKBvݵ |Wad^|C\E%8,B:p.n.aӏ#Ayg9ܞnT)?UΗiL&P۟t8zP[ DToz:2vg O[g`bnF |$7"5YoZm-[v=Swm|nc=35T;\^"1J5B]Y)_s]h(DfX\εab aJ3$(R"kG3Ui?\h]5q >:VQ BF݌bT@h-W ;E=;]DM$( oa.ie{=qi|dˈ[{ (6+ p\uD`D?u)Зtk12uX~RKCAc><[ ?K~_yi,ټ 嘿xs(A%F"CdFe',iɌ×zN0C22sD ?  @ch!% Hf- q mGY͚b)Fwa_"#a^g /sP˃mo2,^c ~ҙ|j?8n2WX[.{ /4m5g:U,Ԑg Om?+l %"\Bdp 5 ǚt>Cq4KX@>R~ KENH,} @]p)̆ı6LZR[8OR2a D̔r e*jo~@c p݂[S.Bi~bQ⢀NS\ØY^=x7CDWnBT焽M(M/-ͫD* R7CÖ"<Qĭ(D F3ZMM *cnފ`GQ~d݀7P 3@78 vemO4-3!vnKΞLHxDئ!S|F @ tAsI`DRPp- s NCuÊ M }&>g5Nڡ;XJ%gF)n[̴_@.ηjdM1w< E{ XqdBr#<``΅{h^D,T=}r *'etTżrOa@fp2|ss1*0%ɒQ01@F2 2t"?q@Yb0* "H-i-^xx*O{L~iƨ]k% 1\œ>S;E1(~MxiP~A<%cYCc#=c=a25{>5 d~ I \@fzXẗG`-㯒\nHW],[k+g¤-JdS\ E>^U.>".(1K/ Do?PʳK :.Ȩy ?pҵ[yey^*1KDVmI͌#?& o_X)!տ7p0-kn"GS+O(pҒr&c@/CMnNY* 210Ȣb_wJƋ\9 V;a-!$w,t@~dFa0[qzњ/YH!i2 Wm*%dŚMԧ/qRs%[ @kxzƬGlK0@!3Ū HS$wՉHs8 a7zywԀ*yIG~:Vva3u0KUJ39T@*:oRc).Ԍ mU'%2U<V?!.1YS]ED;CDz+c,)}a%nGΒ Ȗ'[D*go)_%Fp9ُq9Tr#KIr#T$qW;KU)ƧqΜn0*mԾL~>h.r|*7{٬s܊MHUk zo@o7Yɐ]X!`'++toYhL)(PTVbЅp6'=@I#_Rmۣˬ&R9,),8;Xj#'[g8bԴS[2Iw=A],ݔ G_Y&Ւw"dA Fj({U̽n&K*Hqqb"^d."YĻT*j Yj(gYOtZ2a*e%1AXi]HSLX{| 3U #oF3^JSӗܰf[-9 [d )E$gKHa>P֫?[%lM/6^%5F6 ]hc/`"A;Gٞ/X]&T|'Dwp *jGw>Y:~ɏ EDoUipMIXn<ΦIm&ŠU6n"KVG8#?0yVmNUW609^н9 ;MVN~)%䣢f5lPhίE"bIRI$\9r?@cᾛnpܣKW|88` !q*Ozފ{Zrw,C k}|k"Ck` \mP;%H]a%c.CG ztZqpL1%@Y4EW-70h!dC522uS?$ jCp @Ssy}0&`״c(9B[Y|!g:E,SR!)Whiڨ~R*t٢Hsc0rcᒧ12VmacȆbRUʝ s[w&_Jk:\3kmoU Kń=~)tE\4- /մhӇ-6l{ uP%uOHx~t2:L\r'R3 S;/ }uPkC@*_ $cD@2Woꇅ`o,紅)Zt!T~B[" bd#<C4H"XhW&[`W%\Z~T]Ŀ8&Fxcb[P#HdߚZxrrTN+ @+)L;}#r #2)Rvh7%΄!AZrkF]E !z$J˸ŜyAҀE2@S5w?uPȾLhҜߟ\WևlCov ֺ}xmKI/9BEP-_>Ę{榃̛o$75SfΞo/N3gdK8ӏu0=IEe 9R XѠ!߫ģE:2 2vD ?@ Ek`8b"@LiLa!Wb8*!ؙB*(@cݡ#њ,&)uS6:%uPf5.|ۥ%n<{KQ5=a?* Ho hQNi\˟xofcHaHct|DXE_G8g=cvJm9HTABgpIUńAoveg8IMN!4|36|AN7wR8r=^, u`s}%eG*X[ ˜&quL$+Iwՙ@~5xX3gç9ipRr]pem`(d> ^j|?jѬ;c5s)iU@Ӌ&hɢQMcD %ꉍ>dVVLMܜhqdwKٸ5j!o_`UHm`C<:.))Ś q\GS;Vh,͵fεl'9\Y5ӻ`fހ&h'V7ᇒyW4jؖ7@3CUҰ>phNq<\E#V<uoW1>iBǩ 5&-Q{b8(Wz̫f1s+/RQCgʀtwNJԼ8eXٝN0P[c>¸|4Z،Ex'da$a=649=7]9ymZJaB֨-n)B[2 k{D4}[e-/6\ʇ N6m 0{+Ĺ= 7m- ʾ5UCW?P.1~CpRI)͗$yno$;Pd}dAZyl4a!o\/MrG,bXI]IBG= ~Q2-G986|/z }LPvizԁ˲E'800| A{9LjN?ΠyEW? ^ tkT90]0XF#݀of7kYR&CK2:>p#N6;ܚavr)ghe( (b>=@%jvq?y}SHۓ!64 U|>8Jw O nx]ii/t d pkVjQp I Bڦe+:/ÝF9z/5}|=Qf=}nBܠs-xz4KOC}L!n.HblǾ,77t"}jrg'u|+JʥZFlv*xw"b˰D4Lkka^RqtlUA:5o?z6o@ŗkaXb6Ų*.f-f(q}P&L&\J8Ks++ H{/ڀ/j ,v&"^Bƒ=-@ʗԿ[,\&|?D$n'8& |ReYNL/ťBU,O}y \"RhC MYhDV)1 O۱P}'$eҒ!5~RTߋ}BCNf:f̌xĩ'!u ޑJ9jlGd.U)s"'!`mjf7aMqz]'8'kuIπv]SPYf_vn2G)* I_ DS* d%R(Yqk{57.GxQ9)dQ]w/VK,)-ԫC$hivB90lCK_w܃U ;KV94b&H7& sJIq<57' AOlkRWg*2'!<+J߆ 7 p,p@Egΰq]y-J=tkqymP Qg*o gyN}ZZf`"FG~f*\},O.Csn J4]-e|MdS@ajH*8K:xX9k]|ug/ͥM0C $bVۚ!}Ml<~q$ <.3+ Jj}Ǽ.oWq~pՖioj/'UV5I5#teV`1\x%yg\,jzL?>)ޭ!d{ڟ>5{XX٩}E㴋AUF<>(ZsZ7*+)2/;]XUqi4W㼞-e bRTNì&|$jzPeo>llP˅̧yy=s1y!' Ub i<.i7:,Q(@Tx|g`%>=@4toثJg0NH6ossck4D%h`jXEbD㩔 S-3{RaR) 58 m.;)R5vUР^`>/mf擴nؗdi}Ǯ=eL!J=Cf`҉94\gZG7߫~T1B J%.x Qi*v~q#3L(ȭCQ2wO{qKxgfXGob`NURxiET7}-g7 Ivn&ͽb8!3Tidĥ0s7kkd vxI338,u,QiI'^!j"uԪNzXw\1gJ`~t,{Pcۃ+cMN#,1,dXӬ qe"Ĝ%@J ĥC8lvnd6M+QTb_]v& l\9[*jy'/d]of<6S)Ƽ#B[yN:19CuF˰W-_.h2NQ0'=\$R\&߂ߠ2Hy8x^Wr ?pS@K¬Roxd2 ^+ ׮-~OH^?e:2}!’n)j|:k%CTg&/=*]GNy3dq3O(mKݦ\({xy=nS5P;)YT]+_`l_{CDb} !vy-MԞV. ENCίix>j_]K:3Bf`6Ji޶C%]|a0{t@l'4X )%Rx s줸\`')ߑC=@Y'9haNv<\5<@5ڒ?]/طr]B(#zB OWm UbifV'KzցйOF4K&9 i4Sc9k%g'-&1"_M}`{ƍ$ob+B"M? H.aQCnr8EQcmɹa*%3>X..@R!p@d59EV)Gg? jZ+t&_q H0u""nOx<-FI~RدXfKanjK =F3s%ɬ81;7 u(jr c֎Pc،4= !"n:}:2_w+|buK-)y&f%[M8{>4~3Lqu!m!ҫ[hKK-KwPbk,U^znX N{WWl~ Û@߼j)32t[ļI|ZECv:g>x{:H2)3c/i8 %`Ð9)?e\B[#zO.*.yDbf$,z/ZEmeii.cpLuA@]͌ 0WA&"/K"Ja)L/kae]Y@>M&#[hA)wc!pɤDOP.) VDuTT-je+r vn1W3M5\O9q7`ak%-qzQ‰xYP4~Vs^͓WB4~C2pI5J+uU O\y5}F." ŖRF(`NJyCl$8^zZTR@8QЫw ;[Cp*iô'cLgsّIsKhơpCi^(MHq=2\A5YbRE19Eg>0y;Pk'f<xykwg@O+D_`&Nd5wnR M5U ?e GAc"̃n>?mtYtRo]+RvF,n([\@<*͞6dRQ~v3Rk?%`0M 5ܶFaCe/ĸX`֟7`Ɵ`|)D|3 #md 2(1.ifQlD3ܟH  RLF%|M\Nc?<}5(M\D9^BɼHy-/▵e Ix-nluZ4UBţ ND-#Uϔ7ƃZ-g&T(3gOFN('m ikMGY<{`οY<{U u<Ҡxh ~ |@J$*nHpºo-GƓ^\$;7[S@nTbvj.)c|/Ww_^y`;O/{"+vDbZjӆE$J*iŽI(e |E-[6O_,%4GrRLD{SMKk8JR>wl֋nZXOeXG>4Ng3u''=銙Z8YAVU gBؕOӋ{S9M/#axuDž(vðt1 B9ЮW.jxT5RJC:a?z:ᔏhp1fy҅VNp(F[=>O9g?4(5U[l=aT{ ?#k0 fF>C4d˳'DzN0k35B5ullqih! El*(C&ሳ1y.bvfK'^y 1-%Aʖͩ+Qzͷr0DinJ(T跐r1yn[H`$5},pHO4`$YZ w}LK% eN:=@mLyQ4FOX9Ϊ'e|~n\7}m<]޺ȧy4cƒOl[22yV:X ԏ2ٻ9ǑhV/S+`oee ft 2#45Lt,lv=ߺiE2pHq+nR/Nč_h.SC8Բ_72T,z"D|[5+2kyW41p<u5sdqA#+&2>jrwWบ.ȺmI~rp2n5oaY}Do6{3'@naK?-ȑQ}hxK6/:n{WMyHv%DʆEXIz@p~ϸt410}Hmc{ -xX% a, @ YO RCUN쌄 ="|s:/T@*gL7 D1=R]PƄ.wIk, A/aW.{KP92a_+pŦ^sa;"^ɤ\ iBP`2ǎX+S*96 瓔uKgYq.NZ @.3yi}?m_͐mhT/9x2&Ey3oa>8o;v{ 8wϗW.|fW!RaCГ%Q FSNHeIbiyK`FC1?_|&+?#FCHnw(~:7Z飔pcDWcKhja{emLk]M-3tx<:5P눦1{2v)~ q'Nyh(G *"kj^@O1=joč19-aY RԬCM2čD@9GC>>ٛ Fi I8XR Ԯd!d/tܴk I)nQى}YTg F3+|tx)Eʃmu kl;ew\S$kqS1l_iW]TLK\ar᫿6%8w3|-y<;tCZwnVK\HgkL (˪+mDWL;g 1!<p/Q뢿KEX ~hۨx1yT/ws+?l*o򒡀YK>UZ!o~l& j FSG_ mc%-zb Z͖` SB}22{"@b*F{ @Q"SolMeP?o|0-<_h ApfFdz3cl] ދȬ$fI' :0r.&<˹yNdg9e kރ7awY?R13ؓvv- ӊrdqfvn-Xܒ(*(0uܨX+kA(lHgJ;SPQyxd?҃۽2]<XDHqT7N-Į&jе"T|Pf 0wmv^"V/BJh4p#=*7ώ׽d΁kkNuAGq#P-ڸ6䪻)&McfK%PćG1T-}9j]4>wKI8 IpRhͿݗ]b'Y~8 ]*/k$d'QU _BC#a5>Uez`˶oNJw(õ /@gOP%OG|(yyul _޻]G~,,`P3g8:eƓN.iCٯA˜Z4hs&Q)IϪdb^4H}&6'*LR4p(8ѕF=pFKq*N$ JU$p: ;}~ x \KQӼ~F2 2|V aC @@H(fܡ2'bU| ݿ'>4dNUR@ k_VFNp#8d!;tP-_]/Pb}3G bAEU0S'A~T6Rc*VME淍YDx0ῦx۾ ݹ~b;^z%~UWQ9rX/nRkHYwV̙݆ǿ 2׺2w寉-~5@I-4W:Xv(wǓyÐA_ v{}]ěs*ӻgXʉŰ!iO'vL@hiJ]]ҭ0җጾ ^ KaP͖L+ݦb_eIZW(Уͣy+S6hLgRqYS L:䏃Z }G ==/{gĝ}(0W2(vP7]jK%2 x4%${R$R`Βv,_9gggdahKC⪔5>8ťhepSB 7PY~'NkY7{l&|c6 yGu8ޱr#|Eoe+=9*';@4`%&\k*'kA0R?)kﷂwOȈ(zŐ/Bcm'-3k$u2`uqX1 ԕnPol60Ƚ!v~K \a5@_j 0*Hi-`O;P$knyvhQ|%KLe_W!O@^^c.E$YbjDà T ،ϭևXC<^c.OIv;ۢX]xzUO~u:Ʋ,Σ $ܬ #-m9g޴hb=!F ˀ#CDK]6|a&ZON?~Mުdnk.I_[ga}H޴!2 d{4=\,6Lcb- )?  TU=\־޶M;rl~EQMP_9!sH*`o,%ca&r3+‰cʓL "% R l $ D u5z{{> eEBEaLGjczIF`d9Moǧ+кmn;&%i?fF|_85kq- 6Bx2l ?t/rR4dTǎAW}9/>@^|=-OAh|t9n '㌼ !hg(Çom CF22}D@q$# hFSm=3猤<f|uAȻQE΀bvDsc[>F6prn*퍟-HWn۸ -ݬ4ci}ףL)1-Gʲl+D 0$ظrFtxQ`F؆prX mN/uLd+'OodcI$HO2 =V˒n.,)ݩjJg(T_K~_g܄k)E,D\N9NRr_LR?,/=zÂD͔="I}>woz)ovԮVD Y)d9X**IJfkP9fu>OaEoKW/(.w>w pSBk,Żxer?&lie+ KdE Xa@гG@~՟CS;zT?xmcQ\KOE+NaKW6FƌI~ʌ7Slkh+p_t/]V߽,th\tF?7d"g\Kxf89 !>0Oکk;g!!?0qj1.J j1}Mjyg8 A0,o4ߤ@Q8tQgg+y`Jk@giۂ7PίpwI"-74D)V%7ׇۂvP\:̌y)`K{?wX֌VG+mGvҦ+B_FKML O*Ft>sFI.pЍGުyٙCF3@߃חQvRmR`@ڈw=h^~=Uku>]8ۦ'D7}5䏯HSl| UPDc$ήEvEֵA >Eh^^5vє6z8v(MiN;CUD>o>uhїxifW-b: Q ](,7P3J[n h8CE*\y|4"yC ʹ-<<h;'<$΋JNg0Bwͱ4'-jX_)kJ=cNI8{Wse/O)_vt鬳ۉteǀ\/ǹaSޙ^rxO| ^l uKg֞\5MEv(w Z #V/€B$22VqB0 $G@ -U=dI@l;K06!;rʧGo={e9Kr=j~^a)}񧇄m2= Fm 2H[ ,dq/d=gQy]`LA0@1#-n3F_%BMCblgʋ&e0T' RP7gID{3!(i}̽wU1 "B#cs1c #|SOf#ݭ9_ޣj+qaͱ?Yc2?t23jah0䖋8l5ul@i],p)dq"r,nM܇W6V`4dRY,@ܭr,uɯ hIz}kaÊ]l<5^AI.!_سJ0\A_GaZ sa)%oyN+owbgKQ.3Sx:HnXv؜7?LT``g!S$Gh-@V6v:"hH]52  y LX|vḬ2l׿(Yo]*ZaflH0\.ڈ^JB>U%ǃ݈>ٽ=m$2OŠ21Aո秈hV Nr,:8Et0237k~8 g "Jq6;m6[;o*ma$0VkG= |˲Jf~c 1}cĈ$w/%),ท۲=fQ!=nTtf i4s06)!eNvHonb]]Wt::İKR7[Y`N@ף >1ys6#'$13фF¦`JOkӰ#ˇ跈8/;gׁKLJp[ {IuH:Nr*x[s\YW6F٠ r掖?m>E;CZU:t=/@}8OmLԄf"rҼ!f_ 13;u*_iN,B&gA΂K8:'}tk6rQj] TRrwy:MjQ蔶¾DXL\aߙ_l)GnKU$  /kc,M7<0oSW}O=o=VS%MmP5CܰU݆ ]W\"ȝۗQ-$[+TM"1O#ojc넋n Q˻r]PGbopzgk>Vs _ d4Iу֑1I#[4,"u>:|GlYmw2?*h *4e*JrJ÷L9HE`SàXa+Ր[ Rc]_h O9*t1+3(ODpA6/5݁O){qQ?Tׁw2#!˟7@CB`@MŌgc_"ja?uj$\Hu\X cG#gpc`)i6+X~wӃ䊮?O&N)>|-UϤ5ԅB?P}ȿ1[JB*K""KUKL/pi#(]i $VU`5s>'Ȋ_4@ٍ] Yz;KfD(4d` 1v5> 0@z @aghLİ9tȈ[?p{r[Hp<Ә!a2! ǒ86BENgOCz)˜h !W"{]Zt]Giv7J=cfv}X6l8F &wo=h}nSOӿ|NKիg[`%jXϱAM *j qe>ZOsLLك 'R;5,0ƞ?t ~;wptvFdw1d}=%*P&ZYyϱQ( y}ckEӒX,:!q__ܲd-) ,~{OAwn1>#I.ôhu6|uzIO8SO^YZ%r#]+~7K4U=%$Eڨza$J|Xq3uoX/7{ͷo=_ËhbXF ʊ彪mGd^3U{^;Lf,j%T<ީ{W[݉YS|qfu hPk#B%:my=ز]衷C\SG/4=NGq2ц`yrɶٟeu'תH Fge``[!21 @&΍c?W>=p]m@~/=r =BٖH1lJ-2y7|7J1Xdk_Nw+-qտN_O)e8`.$ %^Vt%X"t#) dl'jڧ]39HD{P=,D%єI'??whg]NoK>zjXeD5S8ݚm+HIH*_#*`||}HE7Fjo߱͆%:ݻ{cRo]H>cϼO e>$Q(Hj]*Q9 5D))'U @ r_mIum1"'(r$GU۹ޮ2I8]k*rI,ϲI-˂t3a-~,]I,伛\R_slM/Ì PN9V "y +(^gSB/͛ӣp| 쨣NI.?!5W`l 7{C ȻѨc!G'UW7ԶTެêna{b^"i$rD6+0ӎnx0=ԁ3I#7.1%.!l`)4Y5[JZ΍>6~^8{ZQtfw{=c m}Zkom6}m7\d4D:CIй" ?$5cX;Ӿo69?r%l)Upw&6 \12"kGIY?!\Uugv p5ւpǝn5ѿ̫^<}iO<~Sgqs:_sJRV<]賫zNV@DŽ?&>7?$F{N|[= @Ouy?"?׭txwi38Mk3 Lܜ,OXNtA7 КTr΄FrIxz j7+k.xcOCS^:1]ŞU +wx\QWѩZb% ٯ,Eg-).q]!bsz-`j\Z V=?o쨨&ˌ}nMФLhHےv3ݠwyqJ6](8E@Joh]+\ς1OuJ< >؞2s[M003vpXUh?ݜ%4$)\<]מo%FAt-65o@o UIŌ_T%?ǢD:V' *c} iqޚ.tF-mB8Q9/ 30O ?V-z&*rSqTq |_C6^1'Ϙ};u}T{#s*iЁ9q?7u`@E$̢v3H@C/==(qcXI6(-U8/d!Xx2b1xڶ&`U_ee]JTXəI\]Ru$-X @5 On e1j~rYa∇5DH ~[ ߟ2@=;za3g}c%sZnɱr{`'C姙w(J570x8,pjلIQ dE}#Oj@ {9Zfݒz:{f"bJnox̕"̈́!SЂ9B]8Bb()0dUkػD ߑVιKxqp VmԛoN+Y[g-`/uoy7Lj4eTVL QHFOS{{PHH&MR,LlܤWmd@EIR_ƛ#Ѥf̍ӣ鬰pyRi}B/>P?26vZfNdbvFfO2Ss9 Ku]ڛMP6JGIx˃sN7߾HKInW"44%д47_>!犕FQQ>"<y\B2d+E>+ŧ8|U:U$8<]`JH睞E਩}~ߎ1\,IƘ4K*-zo"Ufdroc, ㊣v 1/ mm̻; u-tD儜}Dz rH0Nj[j>s~H<wdj`(wZ7t$$&IU,26ɨ,9i2=̩J+z>91N4ydl~d!_yH Է1abͣ'I+/lЂ~":)g-wp5YnvjN|F+Q$e;#C9Ե;䧬6!{k֓xӠs`eψ_ETޔWSbw}c`f*aD]oֻzL s)US$fJGν}Mt\ۡPknډOpПe+~A|Zۃv$xXno/q&drD,\2Ph%vȘ6Y\3ޓa.PϜ{ܔ'tGsşan=aVK\Hn鏑EVK^ԗZh* IBYA! ,bO;6wܩL|Sb">2d)+ի!,6owDK{ 2Kyb,/m2頞ѣSg=CFЦ ۱i)Z *w&FC;PX)3 ]~led8nw-@I~j{1엮[ $sbtZw {4E޵"u)unN{X,ysR+"` `ύ$.:TE¨*:ۗ6c)Sӂ\taaq //eRm}eAEWoGˮ[wCcc^P2Y;UV_FBqطyh82nbgq,X(W[|^X"|B/^Q6@K/-)cE'R 0.#''22D ?@j d4aRߪHmָ/xo"I^9;_ł+;'K ,:Y-9G)|"% u6m䫕 +,m[~&8@=10!3Kt7rȿ*.GbImD`StJ]UWZkH=%{945z@ܺeB!soSey sg|.yH2o#YFKO3+; A^7(id\t5=jS8IV暜xVAya,$!2(uQel+MzHf{ j=ӯۈE]E2 2"?@@0b)+iCn`!lpfd V5!r^:s,Ӳyt5& %-5AGqMo1iҫB.[,DUSK4P(fhB`/XUxGdu#Qzm󆜰Mp69b6|8BN Kb)4r MC 3ǮT"*Ls_Cmt~Xk7 .qG QJfG+"PـF/!'cIx?>vi=Jrr\ۺz9ԇ07oGlh'XoILkAR6älK c'tK~߼^_:-k<P<'|40S}@$ L/VPteLw.(*(Ο(:'#kY ׿W:B ;q ʮg:8rUAy.x7'$1Ϩ}Sf^.KPOIͺ1[#d)t,r5PlH@,ө{ovP H}p<<a\rÊaE%](O"\){!8A 5Q(* A#NTB,<>ߺuelq<*n򝾄 j?mwI;{:Qn%碻B|6Be5anid_%XY0۵0v,94vUg~U*%5kgN{,YDOš5QSWBCrX~XH4< I>'3RhHrqQnUWI9Ucrؖk|9Ebͳ[^+O2F\/a +McЖLy m7y'W/dMh ν0*KW 酆10^Fc[cNB%,B<]ެaK,8;v+l?B}ԖZ"&?/L",~ N".Rd0K۳07%5FM*>c^ĝQ}X_ݴV4^ ` t 6 Oz-6թ 70Nw_SuC}I_x>A;VO4n͊8$! _ZE)VRTeY掰rǔoJJzGw>T9% uWbDiS'voSd ;$+۰q!|/C522S?@"墁qU\OE]ϛ.e't-JTZ" F9#_ʑ_ Dowfbnt-H`H6ly'OerZ\  0ڙD,m$nЎZsa͹X/xj'ӻP:Ԯ)ŭ.GwGԍmDZ9#%R=^Y\Q kwP 7cQ:<̊I&Ph׵q]cJB#w- gG"= $EZgY~`WMMr# (_IPf0;R =iY8SY&/ۣQ:-uO-及[nm9ڨ`hbrXP$QTBNF"P,8qh, V+Qk:K\!%e⦚At4R~wA/xaF4In nb@z&x*BZw0I2B0GGA fMTjP//ȓKA:d(&3bu{z{1mln |-oD`H)v7yƷ0niؐ.+?@|zg#*$B9K <`L0F;ۢ?jK!7T/8ؚ{}<|g;ClT8Mjٚ'KӔS\v>G5pzYd.X*F02 2D ?aDY bE[gL_E,x]kҌԵ^}6mS+{љ#F>WFoIέD ǭ}FA +q]7j%4Sh6|3f G\AYˠ79IBwTTGj3btk5Uw߉ 79jT5ő@KYOqQg?Z%AἦiV# 9$ ?jv8{SmhTeCj!.^EbW$ŠAU-,˼axSgbGݠǧ:Uk2)olf" @;@9̀6({!fϞB`UU|i.6/.Rtl [i.1ܩgbpEJISC 0S F9l,IժE@4-W~ MP.26 (fbKX{b,wyV9&V%4&w]LG5P?kNMBG4bQ57LH`( ?_0[\Ui& PDz@-q: {ϹLT|P2G`ihg71U(԰ň7KGPͦ,tlyFE`]Q78pԌ['_Hn G5 ؠ"B骟z韻V  B22"?$`ڀ23NUHںbF{B}9luk(#هB̺߼wغo$>WL>lvr+w+uxxވ:F 4A8PrRŇ)Z$bXRaS_|$~H3Fo|\3ȮujVYNK\{t6X!e:HVN8z.7w9*i"u%1u{='h샋\@ߨam )A'-x;;ӪR!fa {DT@}ʹ1#2(8Îx5dzb|dDqRqQIL_|9tz|^|>ˬdarl.~nE xRTTOJAKRgS$>2W. ϷN`ΐ r :/F:!+B opMp1c%i"Z4 47GkF` ԝ0"&f5xb7JIUq6"N[Ϸ;a M*^4GGL#c@Y?/ iXkqdd&+S,fQM,o {M?{x+A.B8aڗZՍyRO % T~AR!< kJՋNGn&^Lti^pbBqkhHƁ \ƀc)b8b{)†iQqZN8s]jfZp/M{) )H`Zl@:IE92 gh8"׿p{0e(!E݋T4]lڊWU}!J!tf0iCoXrt}he]( 71>۟Ռ'VG{]ֺ:F^oN9<{3 mÂ|cq4}$ImGPr[CSCJ;d=w8aNˉ;-Gry҇XLs.FzPTLu ?Dہ2 2h?@apb'@On/Y%x[6w錫(q|} pXQذ us|9(Yۤe!ց80D?y$~"eodsE FZkꉂIU\COE5T8LZp}HZLi۹\~m|lNuC$AV*<#Aat,!Hx@h"9e[]%yG8 tQE3G-\=;!/?P׊Jk$ ѕ'4\鹶[nU;F "v_&8!8ֵ'+eBܵJעzfNm L$td"Y=7Cee]L5y<ɋ/.NE=H;',Nե0m# ¬7Ѷf&,:>{C Jn @[Ǒ_1h =W pÝX;fǻ%ڈ I%lw N˴DR0Аs0؏na1c!?,f/BFtk=󭹎zaBQ_â2_Q)V h jz giV#|+7!˽@ls\1^q#[rmd:tl|pأ6vB+f9WPᠯ)S6~T )a1Rڧd0ֻ7|&= {ɼG =u.A&Lgsd c.t|NxLb2B0u!ޞ$ ` K ߏO-*nJ'G8j0"F(ډ9=WG\Dt2}#hnJӃ0)|y4rEL*9<UlD87A,́oyW9)bw\ %;ѷ8so;,LSC@>9 h3 J#'_1oVaߧ1H  kE7mkSGCi+6_6su Sfeo&5#LӜETcƑ6!rjftRn'ʁ#zT)IVҒ_;`3/x}0_VW:3W[G^z^2Y1 (C`فLwt'fgsS~2"!CJ߆ ԰0 sqH0 4)&"5"1]Rt2J+lJ@^V/ZDo0g]O| _Khd7 lݜ>ʧ/ZM"ϭԉA"-6^xl;T`,n+0dB (n9xc;hMJΠ֐{Pp ~ y31T*~tKS u'?O ,xdK65q~!/rPhVDpk"Zd08e Nb(CvD -"Cjֳ 1a=6ꖽ ]ʣ h^8=}kp!r 0D\c:wnͻUE`Yeq&rgΙ/Gyk-km i|fui (5P hwY倡BɎ]".E^n"2Gk_󋭾 ^O'ĺB YW E[Uz$P[gn*L s= $+@.1j7Cx= _P}R[.x7I0:VIn`?M]bWԿ'$?F8ݦ]٬?>#IОYm}VCEf9}NnB yxn-rF^4% \`7jO-˻[7t@!_Яp F3cɳ7pIr\庯%!yRZŨMxqYOwd9)p";|ۏ#:Ŏ/X-9{yF,eCJ _#AJSH,R+:X>w{]LzexV*l%}es`Fa^,҅*8F&詩V/Ϡ/ZB  mlr~3UX '|w?W3f㏟9 Ba_-} tk ִ@Q~Q{l+0K(XRvA㉠,< =e(‰+6(a``VtjfO F.' ՐnH-@u'ۍb(x"i8t!zpZLPQ* b ;Oh3:|3nɋY[b$Y݇D5K%#HQ5 dΊR|U0('e|l$`vV;1n4{ZKr`y@ ~imr s+6;g/:VHa\~'z^^>{yGc*rYIuңa5dij- L`ڣD6~ʩȈ!qy-1s&Yff!؝N`K"*b(F܀ӌ`cK4u0yaVňw6#&~ybՎc pӧ,yOvP33WO?B<(2s5^ [Q+@T;jOh?-C"B,G=<2Gk^?f(.ȕ@E%F_Դˠ1Q+wƈ3&[yҙ3Ҷ6/k[XBidA\)^Tu~ u tߣ F1qR6vpqb:.0d2Vvȍ,T\$^Br3H kٸsg ->6z1${u9S &-pu8+%ln9cռ'nzڥHb4 Ⳟ"",G NKjШOct0W(j*v6 v-Cd~~Ya"dYhAal+iیql98KAgN&QC @ݩ۸d NV#t59v $+ł]B 4yXiiq' EuDsA[3F(<$'s q}(.ʇD({ с?i9F˼pī/jvI QC͔% _d[-qFU^E>%G=YOCu^tӅ%%lhq%CXH#l 0"x[Yls1͍Ta\tR I+M@^>. ߏ`KDFrt=YvՉuPϠ,ɑ(B;fc?h&4y#zs~p-NsihpdXDj,XψU*bH;dτ74UU\t_nA75j4#^2aBy/*j2༊jybQTs9o8]7tlH &iֺxh8jR0&yW68AI^ m]֣JWJo GEBܜ70ўA&J!)PE7ML嚷}%(M d똼-_vE7$ꛕJ\;;76ՈKl=&9: /{=_ ;{)B [Ubi_ *<tvd*=kאFWׇ;Ԋ0m Bx AmIsRB+1XKshR9#+Z2_ڡú:"$טSH/c +;r)ˡ~",Ȗ^w︕P1-{!"%y+&œXЇ?jl)~4OV[W!@a"}5=. bp3Qr™&Pvܭ[P^ņ)\⻤9Ey¨597վ{tNwjlēKmY ֤mseV k ؓ*Od (z]3*,)XF'w[Y$L~1n([z0{9 V/=J*PRTn;RJ MuFBGPu,0*U+O+D΀hx=l |Ib2** l{~g%@/,-B :Khr1qBiv& ?T+p=^o DVF9`RŎlhy=T *A Y뱧[7*zwL-6,/4OK(_?#'rؑ2ԛ? 3|;̯=H,#*\5\XeF|?U4D=>×k[jG ْV,T h5{f9uq3ó4[([5HQodDց2 2 V@D&2xFCd|RʢwIqNGD x;ڧf04({qKU&>˕>m^:W'9}n!CMe ҫecg( Nmb56B 7.| R}mSz%<Q=fQs]J !k ?)hpR;qrK#5m{O$˺$P{+I2^Oj>!IMɄHryJI( rZxXi^׆wxjhkNmv+~0:U]GJ \gP Yg$Q4<*a"a6;?Os%A맷(Dy!*&l(݃htxMNzLjp-yNdx8*W5О{Ri%~5A3_r* y?jZ)7X8JHq]ܙ#ގJal1!]Pr Na=|0V*S<2;,ǛcoÐ+pނ[ Qq[ cSwIH%E8d`vD[ K)ƒn$ \48kFI[2^B^S =4LYHiH;[_?Xpkk550{5-l[:)d(pxẍ́*bԜ o/@׃1%5lC7Cs~%LW@ÕSk-?gHJ yMn&ngF7 Uk$Qq:iwOjs2Z=Rd4}{+?.qu&3Mz7v+Y7RՙH=\4 /-M'WѨ4bcpqpE]਑di6侏)Y#B3(6Hcƾҙ! ȎKI 3Ot]L/00#PxU t['dtuv."l4m؉Ziy$8p,Q:&6e @-]y[~3?t~k{27^1#)薺ǐbr&~۶[ba1`48$YF{kV1`.E2!yFXmT<8 zCuPЈ$f[ 8nY}+Tfk5_Lo|h^l'Юv/uwfG9zNt5\zR8ClݡuuUv %&ՙ'`DP4:l!$Ub`I 6< wT_oXe]+1(w7)ϡ4ě^+قnzЧN(xl7鏾,h ΅q|2YsॽA}FSf5dͰ4U\2MYRzͭ!͝@k57 r3 䥂pWs iS3 aVzi `TWKK_jO)74.SExU 7q⩜mH,Ib j1Ȯ/ T:LX[O5W2Ŗ8& 871 gܫ2Zǥ31٢WlnE ?|>M^>Gݓx,]Fԩr{loYn CI(4~|/AmdYsl5.MKZJDB l22 VXz .,ݿ$T]F } T2Ϝ8ȑP޷#{י}]++?2L31w{1*JG)X|5 +?e耴a`Yv4O>EFKD]qRe?M+ּ&]LJVw$i ?p:y(gN! ?@1箓|DR!(cjvb|YPeXKȊh`$p:MZu[Nj+(hڜ )@z#P >\nAשd}*A`X r"hBE0qmMPBXu/yգ`65i#Bk ˆbωEkj]|A9Pi~+M CU22D@$r |m hצ [i`_d-H gTS!.6.,U`4H* hE cOgw49슫%2(;޴ ۯOloQ՟oK1LjO)0­e/:bey}{?̦sK˚蕹Z~);L=juQt1{_CJ}dAvś^o,50g\V˥A,?\/ɝ6L2pY `'KUaВETSޛ 2H*ЍoD-|^//s}j3b),qOd&G Ӭ5a 2|'+ 2{9@sTLD\xnkw{=-t?jyBBڶ26drڔoNC]k_:8"=Jŗ v1evg2r|*BNmWZW=3ovz+2L{ypGv?{O#NB@¬ $pWP̝0G#ˋlM"Q2.ac>Y ޱD7e 3SfT 4d+Iaadi4͹NJXIk+~'X]YkrK5.ŠIy<6Me—<ê FOLS 2ϴXPΔ َ/7b9?!BNկBڮ'`[~{,f,Mu,Ji0f~c)ħ#ʌ y lie]]#|[xC B<|5U͏3aJv1fdyvpan3q8lڏF!UL E=8u`j#Q2!)˟  rp0p~(}e_C~:u^׽4,puEb`|Ů•YAF>ŭ^gPyDsyQK1 I+O쏝uU<$z6qqzpYStĜF4_8S?ϢsGhĩn<~Eg,Sk\70y_E⇈@"fa K{96dm@37i3ilI]ޓp҃?C-'AСJD$HZHc5{$^@qųMR׀G%8tm/=S7޽vNX{t@A2M.r ASφpdpCjըM]"~:.C+E^p(PFjZ'_1 a쓝#ג6:=_o7Ϩ;rf ¦Z(}^P[ʕ;ェ<yܭ{IۄVA:c8-,S޺ PgReA/wK̪K L|A䋲j- aczw7UװαLlX>>, ?bN@#SrQ̣Aj>)RV w*3wQ /.A‘.oHMbnDS(\MƸUvJ `c]ݠyFAp``l9;ƿ0K^gs3|:PjX%"Zc1 +]z J WI5SV8cQX8,6}?aX+9vg-ceO,)xuz֡"4Ks’/^RVM} ,uFR}txe M;F$:9F6X-Lo9?9罻\SNǶQշ ֘>AQ.9{cG &s 7~F'#wP@M1~F&T Ķkf ڬdFI0<7@ xD1XDtnhT2,I MR1I.'Y! v=ʹaȨXuG 8ylM7_ˌEtⱋO*$(~8MHXwdqॠcs./ҔE!rE~Yu]N̳e5HdB5]ki2p⸔T *5pӓ6,5WSLܟ-x%Ÿfq kRDe#p4T F˝ݞ# A"L8j/tճH+׵M҅r6ؐ>$I2 5 _nn4:w&{AFH29zKYt_H¾&'Jj؃-7 r䢘]ķȻ4!.)À4fYk#=I| {F궫6)&@R@e܃16]e9)Tkasa]B+W^㶉u36O@# im;2:(^#^22^L_>6̈T %-KghzOP>lPTc^ ,jB<|Y4ℜy3-ng{1$A lq Kfheϧ 8|zXLGm`;^z΂eL3Ch4-SLFyE y_?΅\ wObogI ̧3-V/mq\fS (u jRdךKDC{kF`ĬEmc(sP7#rMaVڰ|q ԶL:whD^`:;>91>7!_NYNxJ[[mEٕqiݠ0)yN嵼hh}-ѷihQ1C!2>\]>`L0SBCH@̼yu}3-0,0;aAg: a|p0ƿvS{dK/c#hY }Ưb_swe!oGCd nj?BP5A`͂vX7I]ʗÿ́(N 22S?aB&js7 _m r K\aP3.b|eiͺ v1&w|C}? a '7vć%t!﬉Zq.~Hב =FB3c:]A8HpZv5=P:A>tX.4Xˉ۬WjI׏~,R{..KAj+^)]>d ׮kT!uvT-/ UM=Ŵ< MrʪcĶ_ lKy^ek+.[ltfF4LSZS^д ;RZkKjMhE0SCW$l]hyhCA5wF.B,R)k1Fbӎwid6(W'TYc8j )2|3 {rLJ@Dj<22D ?@<$>/VS uB:IQk_R4\nПsg2?(T ,%%6 VGal^W̉c</.Lן}\\K,/do^V_'3`g x]a!"vy~W%SY΋+;el$j[H6ŇM(7#(̶SYNg~K$:a]ztxȑ)d/ٯ}tL]ȧ%,'j':Uz =!(BE4BIڒfAǷ ͼo?u>'͇LKHscp*D Gs] `]&!mŷEA(60-j zYj A7lvOfh}iy%E`zyǹ<{7ٷfb8^!m5q-"k8Ϸb|=YJ2gqsd )y?5$+nZx(g287icv'Vߠ_`f i<~J$(4H Wm'Hkc DK>Ϳ}2c E3NQ(lGtvV2/Vx.X0l,r^:M8w1r>FNmșz]E$AdJB l_aTLSYF-`Ew+huJn㶛sܱNT ZCY%ΨFE5}Fȍ8OHK BG#?~͑8*GkH knjFgS^aoGG`1lǜg3wc+vdR"pO0K4+"*B=,HBHQ1m%92\?rʖ\!ʴg6x`u$>Si$2FL!PgV3ͤ-Sp{̎th@D22S?1(" ` h(?Dzʪ`ڇ0k}|_H͡ ?ܤkdg(2aÀdJombr]YT]ß~#N{va_u&RtE۶ǞzkZzFcϢugΝ '"7ϔ(.VXWd; {')OMvuK.5ܖ vV aUԨ5tgb#2Of:E`;,gk,X5b/!IJVHZ;Ug]V([+CnVCGT~f5Lj$GEA VMftuwkZ8zl^Y%攴%,PX/5ooX36e: H-]6d-@hpܶ7ob#l85?xm aoU!D= <}sK8˚ϒڻe݈\T\2$8)cN=%*أPo2TvA /t#\X `?y;ssZSIG4REXfg:BRB\g? 2KVf\`^x-73J2o64% ~cVgnnm/M}OEg W/χqĨaj)> Vmr&D]a;q Y9+_Y} n'7}~QfNp B5FV PocK2f _3/A;!dXd&xuK+O(g%A(g[# B22"?@af0"Hf@p 2hY }r`:o.d l̊ טmu۸7QfCq pBe4 oFS]rT BݸдxDakP֬}lv;($ϩ8]~CBA 禗. sm 飒gy,rvΞ\5 ۥ3>Nh6;YarXF@c1(#|1Z"!![+H6}KF |ʨ ?~=c~dcpSHLɾN">AD0jca;zh60iWTPS eĔpjɭ4KpWfUiU>z:UY1"5f_ڸ)./kN o2>ͅZɋxWU8[Aㄆ[8Q&CͻOߕѽqe\/+ܻeg vȗChoK+w>ht V:"A8)!Qa >(sD&722h ?@Q  P UD26(%d3 Tz,Pw? okǹ ℉=pUmq(eF8txblr|vdF]W@#0:O Eݺo|k+yEφ,=Gj<#ypGȫm+z`35yFzDZx |bWAާ 2Ė՘q1Xaq,k'W !NW(Ʉ?I&nsqNQA:$ Ȁ㧔`kysZcn4 K 4ʝ5d "h1(Jq"̥-QaF4&GIeaB4?d<VgP h.+Γl~ڥccDU*HT&Q^hSⰩ< [ eny+|`ϨbN7IyZmSaT"7yeե+`{}*92Y JCnLy?s714]Q"=ዅcY3,X ÏV1k m(w~xykIAMfDH|ZKRVԪ/q= ITXone_@?T,Ĵ\}܂ٌ oј | { >\un]i֟HK )f@A$V [`'y,4Кfw`ZH8o5g.:j,g-b??h*6Oҟ#c{B#V.]nku]^(hvr\׉I&NJPbF8Ks:w,01ȁ7%EN5j.viEayG_٥PI:?Y O?sg|a :E{U BڷN`2! J߆S @F hE!˨DrWJrC69ud-ńXvFM|ߏr1tiG+&|+YSTrs^n"QCX}9&tJ JmE(k D.CQL>[/z9 CˆԌV(wTkXѭ.x1p{#x43vHW?/PV]?./&%ժޮR_h ޚl'SU%[yn\cB.29u|T8{Н_HLڴ5о4/O{h _X(vx}WO;6Dn:-)@bF[uWî{2A? 5z,Q&|Qj xI3xZJ>r- F?e9Azh/ɥQgHbXN5Jy`/vQ6C]9EEjjm:1y*LqӐl|?0, +bW.mITI]ewd/&j4RN!#=1c{ܭ<uu~HdZ 洫X3MCC: wXJ^ڶ}&0llEۏk]u*k.U7[J}az Bכxߢ7SbZ]XqV,Hl=d dZƶ B0WLa{~NLKNM4_ݻ&u!?1ؽք<. y#)e6{2tBE|[t"1dP3JD8JsdH{rڌvi)6Ss9~|uf6cL❽0B4j\SvZ&DTP[A_nNuFF*U?o^Q"p8$Q<YFqD6)ƻ۱C_#R-e/-a|K(܈M/h -l^AgB.c.yi ,NX PoC! `!"Qشr4ooݿ 큼X2K,6cj&+_yICա;?Tnʠ }9cwqA4"1ˡb1}RUp-Xw5hGtvgٲ#vLKRڱ"u0BI*e\Ԡ Ib}3VCCYE6p2 <҃qTP˪kǦW`ﯟ҄<7hgnGdN7Nh졨 ".{EO3zOnXMEer&+oeVm٬]i^Bz榱\x&TfPmeq 7!$A![Ht)"2p\2N1 (|x OYE 6m9::P'P5C63C^Lp`Ap&lS wDX_0wO͸A2}ahC_+mBܪ3=r9C#8="$T%B;VڇKĉO ¸B $TP.P{-C!~pU ;F8</Po9%,ع31tBInzݶn!㉊\yOGyy{/,vϙ5,R0X8fٞ~@UKuz74]=J8e |t4N0f''BVҶ>=^nJRe,R `.PC( eH%ID-%/95˸wCyELfLܺ!g9z4?$A/K@]6E!>I(b;]g`߻["hM3jWz̥zk$u.[`urmWmղ@Ь /w@󸰩1R~jqVFn7$>wuႩ6C[zUէQ &0!͒!'QJ$T CM7IH,E0db&Fp(4@+Y ՟U规d阎\WFIv5қ5W^B;u7ϗNC̸elSW3iƈbS"$IZ7eKć7n5:OhQ{ #oY@F}7OAw^a:~=%o[)-?I|y&/gd]V eQwC[ ~D@f22D,#xyMl1m٪^2ו.W=v)F`0Pq㌃8rw;{>NM_SU s1)X,|gYz-8aVj:aP:'1r<\xkbnm{gʻ˱CLjxOqƺ4L0HʣuH0}}H̙&eSxhj`n1]5Niū Fw FZml-_Dz{ Jߊ3DQP}R zYIdR`yu{jhv|+$@"k8Y.:5Ī_ ƣRx#t ^Qq2K-NbBv %?CxqrA.ю cj5^{gbQ5Ad><Z(!$#3!@.h>̓ʈǨK9 5 `rq~] Y qS/DL22"@$(3`ay"LBRBc,u )[:rNu4WV[{pئ,nP/" Bdbҽivm;Z?JPWCĕP2I7 cjTVx R5^1YhP4F]nF}FNߚD+_h0[fJ7s{I(f䜆v-i:)X;FqoSLXpUiݼ%hl c=S9k*5~pibO> t_҉-{ ܟ[3E#f#?rk'9UXرK#f\Wj^[x+vBk[2V%0S'4OpG\weG!BڃVMtjٮ>תIzQ7*CTuN(7 d4m =l{q#CTˊ_O ݸ7((1\3DiY@$3z"6[ڇ cs [rɢ@i@뉿¯m uTLh쿺͛qD˫F2\gsla}}aP%ݞZ_U 9"d7k)$1DT5[vd;,7i,wCWy)I?V/}ScZ]Ck=%!'E*wIs΢XibŶ"_p8[>mӶ[ģ FqΦ9$ j{P~솛ܫXWq0oF&,w37=Z|6:ټ}nd?$m?PsnNcJ_`Kt0iT ثݑm尲,4A22V p@`p6bnJ}wd&JZFY0zj6-9Xz=Z5&@o-qtBh4=R@Zre!K2Y6/֍'r# s(̣ݏprҐ|qG>i=:]RC%vV&2~D$ >~MΪLvPj;o \30O#ZQNr)l!YV1cx?3gjs/afCwZ~S߽ҹ="`ro?| !6MeIs=+$,8\n4q-Lq{\bHCִ6PW?QplUQhCq22Dp`Pp@`Ei{-]%ύ!| BU̗rWbhYjmevѧ8Ln4~- y]C%sB QJxq 3.5Z?ڼy7nѦ]6vQ]Srd Nycb jϳAJ^\.0d]VG%S=|RЮGN¨UF?xߣ]nuoO0bO4^X\{"ၟg ^E't /:y%RWC&k#37Yk|8S5Pa4o~š33vc4@@_Thg7 (rwFb.VudÔzX˝1B]=r,SDi:DwNYZ*Ġ",qMwc&8۩^Hns@0lA.)Z 8M6H&$L=>M*so@KZI.S!Goxjv!食]4t)'K3B( x!ppxT T$˾|h2!5rVN_.JBT$=E@>T)Px#st`Pt[my3D3J@HZf&繉z1Խ*Ⱄ@>2kHҵgWl~ۿy.P d+Bme* 2jEnt(6m U5_'_fs#u_[=6烏Úh@Ag22"p-=*#IꞙlhZxPjxY ÓpKYy*]a BX$oYB6',mVW劕F/j bgR_TCdOhsX"l-=au:1_ $fc7b!E*r^lS5T9|_TbA@>6.}WYS§>R_.R'd(ST=ax#c]iƞiѢ* t [1ϵ z)~Ôߏ^Tf,YhC{qh&!%j:US˓dA122V@ p `fmIm3GtgB8v:6W-Z@[22D`Y7[ȣSTҤ_ _yp*ɠ/cJ T`=lp%pm/ Wc:%+ǐp }li?X_*GأB22 a@q"#KPV)OE7uwVAc> _.0H2ȭc\g>⛵dO}|G>Bdv;!ۥH gHkD*1Yt.+HB&:cɚ@R?=_ϓ88ue\hD wH5̯3װ5h,TZ|m3vEIP,R¬n@zp$ٖEuxHN_q7 V@02 $?ˊoU ԟ'cD"Xt5ᎰtK~L>Ƙ13b1@0xGUMO%Zd/u-.&k?(CMihfƼ) ٲG8[cV~l '71vG SME5W:y8 ۾@ CE22"S?@B `6;3Fp~q8w&Q c^ܠjΓV gpk19 a@cܬUm]gs]%&M7ھ&hibl)l;a8K,y\-K+H˕4T%sF)qe +Ԝ(w*z%6kǢΠ%|Lڬ<'jҨ&W]<[Q|z'<=dNZzfwxOijTSיz?[Ӫ;֊@?_]psSQ22B:#(iKzoI-2E<+Cf4ade]Y?k"xgrC; Sh64)5-7΂=Զ]߭a]VO` <@5ܔD*[48twՑnxX~:אŝlp}#|q/.V$Pg’TMk9/rQfV\/^1:i}}kKX]kH8$}zRa߈Qu,|050v<(߀mngz[rBwB22#D ?7J3фұֹ,(˄4>&O/@=ڋ10  A7" '㖂NUܱ;\ )dGi|jL N_7^f|FsSN![58d[B"P+y>0!m&i|+oa9>kGSKl uRK~U'B1Q s(ZϬ1Tݕe9d30jG:ȭ5 !/ HR2i{_+-^3p1m^ZXK;,K$63Y շZII=&Spُ؜83+}&,7㨞8{0,SlMc~}AT^Úo0MjVu{vC+%ml_:Ve+*j&_w#Wba>7+O:!{1x(2)eQ@(-`bIDc+22$"?P$`0h0jPt TU@D& {X20Gg"e32˫b-]WmO{L"nR(hZӛ n|ݕ[ONg'y1n1mA %[$=yM(CHR$q*Akg&<oTw5dx Oh,KHB^CD u-)iuُ3v}ljz(gJ-"x~-V-2fWUnksbU EZ̲NQ5{u~CUv9p:$NA : G{O="d_IXU36\"-l-Ʀsr#j6o2%)6[P*Vސe;}pd`i9/G1 @0U݅aFUbV=w]8Wi<V,TږtZfPU'"y.tm&=e:*ױϱ2~,&[\H&T,6Xcς-q+}Mo*~EXc96%qM.G(AU22%S?Q @@@ c ST& T"`_}Y5'ץ8gMr\#"mCr(ʜ^Rx6Łiޥ/&K))3#-r,K~l>o4&! n\AEl%TkVCL{semOdNvm0@EF{`T;reESⰨBd%˹L.1^%zMM󷓂/ #]a5wo=^[1Ҡ_a_?@N1/Ma (FLeINIY:,O>74@Hme1trW'bdAK׋-KեCF.ߥE58 'Qk H8@C22&D ?@$A=!Æ`8%biSv/Gc791P~-I.|_Zk[xb>u1_u%a@Z*!'@n N|.* ]s#s8"Ey8m==og:zMā_Bc-7VsfagI"!fWJ1ߦI& 3WpR0:o"EpĦ9 md <")%m jpJL]D)?ѭ5)ԐK/ьVvTyi*Ax3fFί!"j52ˡE܅fvgaQRsG^ݑkC#pۓH`ċNԸ≠ikƷīt+q\f)ʃؼmeK֘U%i\PhU~ D问4!R*7/!/%V.ڙCaǹ $`)a0xy)~0Wo*~up5&{s'Q:S/0&i [@2m(I*G~t~3psWdKPBg ǩV0 >Plh+Ev ST):U-WGLZA7bqD[P/~)Q^͊.SoBj(hX-t2[fu쫀!Jڵs47i`ԸTRd'BF>f0w?!]Q`%2Y_b)AMyA0'_%k@jN&%5__w(qZMe$$r(vjqm9@с6AZJ[]AmU[lAx22'"? Q 1$'p%Tȣ̒Lc_; Qۭr"\"FZ9=@e(dZ*?"[ClJ8S1V\o:; V!c̤Y~_R P٠ԫ2/ه|_Σk09`ZKFiCSki appstream-generator-0.10.1/tests/samples/test.tar.xz000066400000000000000000000150001506754475600225250ustar00rootroot00000000000000./000775 001750 001750 00000000000 15050615360 011514 5ustar00ninyaninya000000 000000 ./test.txt000664 001750 001750 00000000061 15044057447 013242 0ustar00ninyaninya000000 000000 Wow! A file with multi-line text! In a tarball! ./b/000775 001750 001750 00000000000 12757035064 011745 5ustar00ninyaninya000000 000000 ./c/000775 001750 001750 00000000000 15044057550 011742 5ustar00ninyaninya000000 000000 ./e/000775 001750 001750 00000000000 15050615316 011741 5ustar00ninyaninya000000 000000 ./e/f000664 001750 001750 00000000000 15044057447 013763 1./test.txtustar00ninyaninya000000 000000 ./c/d000664 001750 001750 00000000006 15044057550 012104 0ustar00ninyaninya000000 000000 world ./b/a000664 001750 001750 00000000006 12757035064 012104 0ustar00ninyaninya000000 000000 hello appstream-generator-0.10.1/tests/tests-backend-debian.cpp000066400000000000000000000315411506754475600234150ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * SPDX-License-Identifier: LGPL-3.0-or-later */ #define CATCH_CONFIG_MAIN #include #include #include #include #include #include "logging.h" #include "utils.h" #include "backends/debian/debpkgindex.h" #include "backends/debian/debpkg.h" #include "backends/debian/tagfile.h" #include "backends/debian/debutils.h" using namespace ASGenerator; static struct TestSetup { TestSetup() { setVerbose(true); } } testSetup; // Test-friendly wrapper class that exposes protected methods for testing class TestableDebianPackageIndex : public DebianPackageIndex { public: TestableDebianPackageIndex(const std::string &dir) : DebianPackageIndex(dir) { } // Expose protected methods for testing using DebianPackageIndex::findTranslations; using DebianPackageIndex::getIndexFile; using DebianPackageIndex::packageDescToAppStreamDesc; }; TEST_CASE("DebianPackageIndex: findTranslations", "[debian][debpkgindex]") { auto samplesDir = Utils::getTestSamplesDir(); auto debianSamplesDir = samplesDir / "debian"; SECTION("Find translations for existing suite and section") { TestableDebianPackageIndex pi(debianSamplesDir.string()); auto translations = pi.findTranslations("sid", "main"); std::sort(translations.begin(), translations.end()); // Expected translations std::vector expected = {"en", "ca", "cs", "da", "de", "de_DE", "el", "eo", "es", "eu", "fi", "fr", "hr", "hu", "id", "it", "ja", "km", "ko", "ml", "nb", "nl", "pl", "pt", "pt_BR", "ro", "ru", "sk", "sr", "sv", "tr", "uk", "vi", "zh", "zh_CN", "zh_TW"}; std::sort(expected.begin(), expected.end()); REQUIRE(translations == expected); } SECTION("Non-existent suite returns default translation") { TestableDebianPackageIndex pi(debianSamplesDir.string()); auto translations = pi.findTranslations("nonexistent", "main"); // Should return default "en" when InRelease is not found REQUIRE(translations.size() == 1); REQUIRE(translations[0] == "en"); } } TEST_CASE("DebianPackageIndex: packageDescToAppStreamDesc", "[debian][debpkgindex]") { auto samplesDir = Utils::getTestSamplesDir(); auto debianSamplesDir = samplesDir / "debian"; TestableDebianPackageIndex pi(debianSamplesDir.string()); SECTION("Convert simple description") { std::vector lines = {"This is a simple description.", "With a second line."}; auto result = pi.packageDescToAppStreamDesc(lines); REQUIRE(result == "

This is a simple description. With a second line.

"); } SECTION("Convert description with paragraph breaks") { std::vector lines = {"First paragraph.", ".", "Second paragraph."}; auto result = pi.packageDescToAppStreamDesc(lines); REQUIRE(result == "

First paragraph.

\n

Second paragraph.

"); } SECTION("Convert description with markup escaping") { std::vector lines = {"This has & 'characters'."}; auto result = pi.packageDescToAppStreamDesc(lines); REQUIRE(result == "

This has <special> & 'characters'.

"); } } TEST_CASE("DebPackage: Basic operations", "[debian][debpkg]") { SECTION("Package construction and basic properties") { DebPackage pkg("test-package", "1.0.0", "amd64"); REQUIRE(pkg.name() == "test-package"); REQUIRE(pkg.ver() == "1.0.0"); REQUIRE(pkg.arch() == "amd64"); REQUIRE(pkg.id() == "test-package/1.0.0/amd64"); // Test property setters pkg.setName("new-package"); pkg.setVersion("2.0.0"); pkg.setArch("i386"); REQUIRE(pkg.name() == "new-package"); REQUIRE(pkg.ver() == "2.0.0"); REQUIRE(pkg.arch() == "i386"); } SECTION("Package maintainer") { DebPackage pkg("test-package", "1.0.0", "amd64"); pkg.setMaintainer("Test User "); REQUIRE(pkg.maintainer() == "Test User "); } SECTION("Package filename handling") { DebPackage pkg("test-package", "1.0.0", "amd64"); pkg.setFilename("/path/to/package.deb"); // For local files, getFilename should return the same path REQUIRE(pkg.getFilename() == "/path/to/package.deb"); } SECTION("GStreamer codec information") { DebPackage pkg("test-package", "1.0.0", "amd64"); // Initially no GStreamer info REQUIRE_FALSE(pkg.gst().has_value()); // Set GStreamer info std::vector decoders = {"mp3", "ogg"}; std::vector encoders = {"wav"}; GStreamer gst(decoders, encoders, {}, {}, {}); pkg.setGst(gst); auto retrievedGst = pkg.gst(); REQUIRE(retrievedGst.has_value()); REQUIRE(retrievedGst->isNotEmpty()); } } TEST_CASE("DebPackageLocaleTexts: Thread safety and functionality", "[debian][debpkg]") { auto l10nTexts = std::make_shared(); SECTION("Set and retrieve descriptions") { l10nTexts->setDescription("English description", "en"); l10nTexts->setDescription("Deutsche Beschreibung", "de"); REQUIRE(l10nTexts->description.at("en") == "English description"); REQUIRE(l10nTexts->description.at("de") == "Deutsche Beschreibung"); } SECTION("Set and retrieve summaries") { l10nTexts->setSummary("English summary", "en"); l10nTexts->setSummary("Deutsche Zusammenfassung", "de"); REQUIRE(l10nTexts->summary.at("en") == "English summary"); REQUIRE(l10nTexts->summary.at("de") == "Deutsche Zusammenfassung"); } SECTION("Shared localized texts between packages") { DebPackage pkg1("test-package", "1.0.0", "amd64", l10nTexts); DebPackage pkg2("test-package", "1.0.0", "i386", l10nTexts); l10nTexts->setDescription("Shared description", "en"); // Both packages should have the same description REQUIRE(pkg1.description().at("en") == "Shared description"); REQUIRE(pkg2.description().at("en") == "Shared description"); } } TEST_CASE("TagFile: Debian control file parsing", "[debian][tagfile]") { SECTION("Parse simple control data") { std::string controlData = "Package: test-package\n" "Version: 1.0.0\n" "Architecture: amd64\n" "Description: A test package\n" " This is a longer description\n" " that spans multiple lines.\n" "\n" "Package: another-package\n" "Version: 2.0.0\n"; TagFile tf; tf.load(controlData); // First section REQUIRE(tf.readField("Package") == "test-package"); REQUIRE(tf.readField("Version") == "1.0.0"); REQUIRE(tf.readField("Architecture") == "amd64"); auto description = tf.readField("Description"); REQUIRE(description.find("A test package") != std::string::npos); REQUIRE(description.find("longer description") != std::string::npos); // Move to next section REQUIRE(tf.nextSection()); REQUIRE(tf.readField("Package") == "another-package"); REQUIRE(tf.readField("Version") == "2.0.0"); // No more sections REQUIRE_FALSE(tf.nextSection()); } SECTION("Handle missing fields gracefully") { std::string controlData = "Package: test-package\n"; TagFile tf; tf.load(controlData); REQUIRE(tf.readField("Package") == "test-package"); REQUIRE(tf.readField("NonExistent").empty()); } SECTION("Parse empty control data") { TagFile tf; tf.load(""); REQUIRE(tf.readField("Package").empty()); REQUIRE_FALSE(tf.nextSection()); } } TEST_CASE("Debian version comparison", "[debian][debutils]") { SECTION("Simple version comparisons") { REQUIRE(compareVersions("1.0", "2.0") < 0); REQUIRE(compareVersions("2.0", "1.0") > 0); REQUIRE(compareVersions("1.0", "1.0") == 0); } SECTION("Version with epochs") { REQUIRE(compareVersions("1:1.0", "2.0") > 0); REQUIRE(compareVersions("2:1.0", "1:2.0") > 0); REQUIRE(compareVersions("1:1.0", "1:1.0") == 0); } SECTION("Version with revisions") { REQUIRE(compareVersions("1.0-1", "1.0-2") < 0); REQUIRE(compareVersions("1.0-2", "1.0-1") > 0); REQUIRE(compareVersions("1.0-1", "1.0-1") == 0); } SECTION("Complex version strings") { REQUIRE(compareVersions("1.0~beta1", "1.0") < 0); REQUIRE(compareVersions("1.0", "1.0+build1") < 0); REQUIRE(compareVersions("1.0-1ubuntu1", "1.0-1ubuntu2") < 0); } SECTION("Real-world examples") { REQUIRE(compareVersions("2.7.2-linux-1", "2.7.3-linux-1") < 0); REQUIRE(compareVersions("1:7.4.052-1ubuntu3", "1:7.4.052-1ubuntu3.1") < 0); REQUIRE(compareVersions("0.8.15-1", "0.8.15-1+deb8u1") < 0); } } TEST_CASE("DebianPackageIndex: Package loading and caching", "[debian][debpkgindex]") { auto samplesDir = Utils::getTestSamplesDir(); auto debianSamplesDir = samplesDir / "debian"; auto chromodorisMainDir = debianSamplesDir / "dists" / "chromodoris" / "main" / "binary-amd64"; SECTION("Load packages from index") { DebianPackageIndex pi(debianSamplesDir.string()); // This should work if we have the proper structure REQUIRE_NOTHROW([&]() { auto packages = pi.packagesFor("chromodoris", "main", "amd64", false); INFO("Loaded " << packages.size() << " packages"); }()); } SECTION("Package caching works") { DebianPackageIndex pi(debianSamplesDir.string()); // Load packages twice - second call should use cache auto packages1 = pi.packagesFor("chromodoris", "main", "amd64", false); auto packages2 = pi.packagesFor("chromodoris", "main", "amd64", false); // Should return the same packages (from cache) REQUIRE(packages1.size() == packages2.size()); } SECTION("Release clears cache") { DebianPackageIndex pi(debianSamplesDir.string()); auto packages1 = pi.packagesFor("chromodoris", "main", "amd64", false); pi.release(); auto packages2 = pi.packagesFor("chromodoris", "main", "amd64", false); // Should still work after release REQUIRE(packages1.size() == packages2.size()); } } TEST_CASE("DebianPackageIndex: Index file handling", "[debian][debpkgindex]") { auto samplesDir = Utils::getTestSamplesDir(); auto debianSamplesDir = samplesDir / "debian"; SECTION("Get index file path") { TestableDebianPackageIndex pi(debianSamplesDir.string()); REQUIRE_NOTHROW([&]() { auto indexPath = pi.getIndexFile("chromodoris", "main", "amd64"); INFO("Index file path: " << indexPath); }()); } } TEST_CASE("DebPackage: Package validation", "[debian][debpkg]") { SECTION("Valid package") { DebPackage pkg("test-package", "1.0.0", "amd64"); pkg.setMaintainer("Test User "); REQUIRE(pkg.isValid()); } SECTION("Package with empty name is invalid") { DebPackage pkg("", "1.0.0", "amd64"); REQUIRE_FALSE(pkg.isValid()); } SECTION("Package with empty version is invalid") { DebPackage pkg("test-package", "", "amd64"); REQUIRE_FALSE(pkg.isValid()); } SECTION("Package with empty architecture is invalid") { DebPackage pkg("test-package", "1.0.0", ""); REQUIRE_FALSE(pkg.isValid()); } } TEST_CASE("DebPackage: Temporary directory handling", "[debian][debpkg]") { SECTION("Temporary directory path generation") { DebPackage pkg("test-package", "1.0.0", "amd64"); pkg.updateTmpDirPath(); // Can't easily test the exact path without knowing the config, // but we can ensure it doesn't crash REQUIRE_NOTHROW(pkg.updateTmpDirPath()); } SECTION("Cleanup operations don't crash") { DebPackage pkg("test-package", "1.0.0", "amd64"); REQUIRE_NOTHROW(pkg.cleanupTemp()); REQUIRE_NOTHROW(pkg.finish()); } } TEST_CASE("DebPackage: Package string representation", "[debian][debpkg]") { SECTION("toString method") { DebPackage pkg("test-package", "1.0.0", "amd64"); auto str = pkg.toString(); REQUIRE(str.find("test-package") != std::string::npos); REQUIRE(str.find("1.0.0") != std::string::npos); REQUIRE(str.find("amd64") != std::string::npos); } } appstream-generator-0.10.1/tests/tests-backend-misc.cpp000066400000000000000000000041671506754475600231320ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * SPDX-License-Identifier: LGPL-3.0-or-later */ #define CATCH_CONFIG_MAIN #include #include #include #include #include #include #include #include #include "logging.h" #include "utils.h" #include "backends/archlinux/listfile.h" #include "backends/rpmmd/rpmpkgindex.h" using namespace ASGenerator; static struct TestSetup { TestSetup() { setVerbose(true); } } testSetup; TEST_CASE("ListFile parsing", "[backend][archlinux]") { SECTION("Parse Arch Linux package format") { const std::string testData = R"(%FILENAME% a2ps-4.14-6-x86_64.pkg.tar.xz %NAME% a2ps %VERSION% 4.14-6 %DESC% An Any to PostScript filter %CSIZE% 629320 %MULTILINE% Blah1 BLUBB2 EtcEtcEtc3 %SHA256SUM% a629a0e0eca0d96a97eb3564f01be495772439df6350600c93120f5ac7f3a1b5)"; ListFile lf; std::vector testDataBytes(testData.begin(), testData.end()); lf.loadData(testDataBytes); // Test single-line entries REQUIRE(lf.getEntry("FILENAME") == "a2ps-4.14-6-x86_64.pkg.tar.xz"); REQUIRE(lf.getEntry("VERSION") == "4.14-6"); REQUIRE(lf.getEntry("NAME") == "a2ps"); REQUIRE(lf.getEntry("DESC") == "An Any to PostScript filter"); REQUIRE(lf.getEntry("CSIZE") == "629320"); // Test multiline entry REQUIRE(lf.getEntry("MULTILINE") == "Blah1\nBLUBB2\nEtcEtcEtc3"); // Test SHA256SUM entry REQUIRE(lf.getEntry("SHA256SUM") == "a629a0e0eca0d96a97eb3564f01be495772439df6350600c93120f5ac7f3a1b5"); // Test non-existent entry REQUIRE(lf.getEntry("NONEXISTENT").empty()); } } TEST_CASE("RPMPackageIndex", "[backend][rpmmd]") { SECTION("Load RPM packages from test repository") { auto samplesDir = Utils::getTestSamplesDir(); auto rpmmdDir = samplesDir / "rpmmd"; RPMPackageIndex pi(rpmmdDir.string()); auto pkgs = pi.packagesFor("26", "Workstation", "x86_64"); REQUIRE(pkgs.size() == 4); } } appstream-generator-0.10.1/tests/tests-db.cpp000066400000000000000000000573711506754475600212040ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * SPDX-License-Identifier: LGPL-3.0-or-later */ #define CATCH_CONFIG_MAIN #include #include #include #include #include #include #include #include #include #include "contentsstore.h" #include "datastore.h" #include "config.h" #include "utils.h" #include "backends/dummy/dummypkg.h" #include "result.h" using namespace ASGenerator; static struct TestSetup { TestSetup() { setVerbose(true); } } testSetup; TEST_CASE("ContentsStore basic operations", "[contentsstore]") { // Create temporary directory for test database auto tempDir = fs::temp_directory_path() / std::format("asgen-test-{}", Utils::randomString(8)); fs::create_directories(tempDir); SECTION("Constructor and basic lifecycle") { ContentsStore store; REQUIRE_NOTHROW(store.open(tempDir.string())); REQUIRE_NOTHROW(store.close()); } SECTION("Package operations") { ContentsStore store; store.open(tempDir.string()); const std::string testPkgId = "testpkg/1.0.0/amd64"; // Initially package should not exist REQUIRE_FALSE(store.packageExists(testPkgId)); // Add contents to package std::vector contents = { "/usr/bin/testapp", "/usr/share/applications/testapp.desktop", "/usr/share/icons/hicolor/48x48/apps/testapp.png", "/usr/share/icons/hicolor/64x64/apps/testapp.png", "/usr/share/pixmaps/testapp.png", "/usr/share/locale/de/LC_MESSAGES/testapp.mo", "/usr/share/locale/fr/LC_MESSAGES/testapp.mo", "/usr/lib/testapp/plugin.so"}; REQUIRE_NOTHROW(store.addContents(testPkgId, contents)); // Now package should exist REQUIRE(store.packageExists(testPkgId)); // Retrieve and verify contents auto retrievedContents = store.getContents(testPkgId); REQUIRE(retrievedContents.size() == contents.size()); // Check that all original contents are present for (const auto &item : contents) { REQUIRE(std::find(retrievedContents.begin(), retrievedContents.end(), item) != retrievedContents.end()); } store.close(); } SECTION("Icon and locale filtering") { ContentsStore store; store.open(tempDir.string()); const std::string testPkgId = "iconpkg/2.0.0/amd64"; std::vector contents = { "/usr/bin/app", "/usr/share/icons/hicolor/32x32/apps/app.png", "/usr/share/icons/hicolor/48x48/apps/app.svg", "/usr/share/pixmaps/app.xpm", "/usr/share/locale/en/LC_MESSAGES/app.mo", "/usr/share/locale/es/LC_MESSAGES/app.mo", "/usr/share/doc/app/README", "/usr/lib/qt5/translations/app_de.qm"}; store.addContents(testPkgId, contents); // Test icon retrieval auto icons = store.getIcons(testPkgId); REQUIRE(icons.size() == 3); // 2 hicolor icons + 1 pixmap std::vector expectedIcons = { "/usr/share/icons/hicolor/32x32/apps/app.png", "/usr/share/icons/hicolor/48x48/apps/app.svg", "/usr/share/pixmaps/app.xpm"}; for (const auto &icon : expectedIcons) { REQUIRE(std::find(icons.begin(), icons.end(), icon) != icons.end()); } // Test locale file retrieval auto localeFiles = store.getLocaleFiles(testPkgId); REQUIRE(localeFiles.size() == 3); // 2 .mo files + 1 .qm file std::vector expectedLocaleFiles = { "/usr/share/locale/en/LC_MESSAGES/app.mo", "/usr/share/locale/es/LC_MESSAGES/app.mo", "/usr/lib/qt5/translations/app_de.qm"}; for (const auto &locale : expectedLocaleFiles) { REQUIRE(std::find(localeFiles.begin(), localeFiles.end(), locale) != localeFiles.end()); } store.close(); } SECTION("Maps generation") { ContentsStore store; store.open(tempDir.string()); // Add multiple packages std::vector pkgIds = {"pkg1/1.0/amd64", "pkg2/2.0/amd64", "pkg3/3.0/amd64"}; // Package 1: has icons and regular files store.addContents( pkgIds[0], {"/usr/bin/app1", "/usr/share/icons/hicolor/48x48/apps/app1.png", "/usr/share/applications/app1.desktop"}); // Package 2: has locale files store.addContents(pkgIds[1], {"/usr/bin/app2", "/usr/share/locale/de/LC_MESSAGES/app2.mo"}); // Package 3: mixed content store.addContents( pkgIds[2], {"/usr/lib/libtest.so", "/usr/share/pixmaps/test.png", "/usr/share/locale/fr/LC_MESSAGES/test.mo"}); // Test contents map auto contentsMap = store.getContentsMap(pkgIds); REQUIRE(contentsMap.size() == 8); // All files from all packages REQUIRE(contentsMap["/usr/bin/app1"] == pkgIds[0]); REQUIRE(contentsMap["/usr/bin/app2"] == pkgIds[1]); REQUIRE(contentsMap["/usr/lib/libtest.so"] == pkgIds[2]); // Test icon files map auto iconMap = store.getIconFilesMap(pkgIds); REQUIRE(iconMap.size() == 2); // 2 icon files total REQUIRE(iconMap["/usr/share/icons/hicolor/48x48/apps/app1.png"] == pkgIds[0]); REQUIRE(iconMap["/usr/share/pixmaps/test.png"] == pkgIds[2]); // Test locale map auto localeMap = store.getLocaleMap(pkgIds); REQUIRE(localeMap.size() == 2); // 2 locale files total REQUIRE(localeMap["/usr/share/locale/de/LC_MESSAGES/app2.mo"] == pkgIds[1]); REQUIRE(localeMap["/usr/share/locale/fr/LC_MESSAGES/test.mo"] == pkgIds[2]); store.close(); } SECTION("Package removal") { ContentsStore store; store.open(tempDir.string()); const std::string pkgId = "removeme/1.0/amd64"; std::vector contents = {"/usr/bin/removeme", "/usr/share/doc/removeme"}; store.addContents(pkgId, contents); REQUIRE(store.packageExists(pkgId)); store.removePackage(pkgId); REQUIRE_FALSE(store.packageExists(pkgId)); // Contents should be empty after removal auto retrievedContents = store.getContents(pkgId); REQUIRE(retrievedContents.empty()); store.close(); } SECTION("Package ID set operations") { ContentsStore store; store.open(tempDir.string()); std::vector pkgIds = {"pkg-a/1.0/amd64", "pkg-b/2.0/amd64", "pkg-c/3.0/i386"}; // Add packages for (const auto &pkgId : pkgIds) { store.addContents(pkgId, {std::format("/usr/bin/{}", pkgId.substr(0, 5))}); } // Get package ID set auto pkgIdSet = store.getPackageIdSet(); REQUIRE(pkgIdSet.size() == 3); for (const auto &pkgId : pkgIds) { REQUIRE(pkgIdSet.find(pkgId) != pkgIdSet.end()); } // Test bulk removal std::unordered_set toRemove = {pkgIds[0], pkgIds[2]}; store.removePackages(toRemove); // Only pkg-b should remain REQUIRE(store.packageExists(pkgIds[1])); REQUIRE_FALSE(store.packageExists(pkgIds[0])); REQUIRE_FALSE(store.packageExists(pkgIds[2])); store.close(); } SECTION("Sync operation") { ContentsStore store; store.open(tempDir.string()); store.addContents("sync-test/1.0/amd64", {"/usr/bin/synctest"}); // Sync should not throw REQUIRE_NOTHROW(store.sync()); store.close(); } // Cleanup fs::remove_all(tempDir); } TEST_CASE("ContentsStore thread safety", "[contentsstore][threading]") { auto tempDir = fs::temp_directory_path() / std::format("asgen-test-mt-{}", Utils::randomString(8)); fs::create_directories(tempDir); ContentsStore store; store.open(tempDir.string()); const int numThreads = 4; const int packagesPerThread = 10; std::vector threads; // Launch multiple threads that add packages concurrently for (int t = 0; t < numThreads; ++t) { threads.emplace_back([&store, t, packagesPerThread]() { for (int i = 0; i < packagesPerThread; ++i) { std::string pkgId = std::format("thread{}-pkg{}/1.0/amd64", t, i); std::vector contents = { std::format("/usr/bin/app-{}-{}", t, i), std::format("/usr/share/doc/app-{}-{}/README", t, i)}; store.addContents(pkgId, contents); } }); } // Wait for all threads for (auto &thread : threads) thread.join(); // Verify all packages were added auto pkgSet = store.getPackageIdSet(); REQUIRE(pkgSet.size() == numThreads * packagesPerThread); store.close(); fs::remove_all(tempDir); } TEST_CASE("DataStore basic operations", "[datastore]") { // Create temporary directory for test database auto tempDir = fs::temp_directory_path() / std::format("asgen-datastore-test-{}", Utils::randomString(8)); auto mediaDir = fs::temp_directory_path() / std::format("asgen-media-test-{}", Utils::randomString(8)); fs::create_directories(tempDir); fs::create_directories(mediaDir); SECTION("Constructor and basic lifecycle") { DataStore store; REQUIRE_NOTHROW(store.open(tempDir.string(), mediaDir.string())); REQUIRE_NOTHROW(store.close()); } SECTION("Metadata storage and retrieval") { DataStore store; store.open(tempDir.string(), mediaDir.string()); const std::string gcid = "org.example.test"; const std::string xmlData = R"( org.example.test Test App )"; const std::string yamlData = R"(Type: desktop-application ID: org.example.test Name: C: Test App )"; // Initially, metadata should not exist REQUIRE_FALSE(store.metadataExists(DataType::XML, gcid)); REQUIRE_FALSE(store.metadataExists(DataType::YAML, gcid)); // Store metadata REQUIRE_NOTHROW(store.setMetadata(DataType::XML, gcid, xmlData)); REQUIRE_NOTHROW(store.setMetadata(DataType::YAML, gcid, yamlData)); // Metadata should exist now and be retrievable REQUIRE(store.metadataExists(DataType::XML, gcid)); REQUIRE(store.metadataExists(DataType::YAML, gcid)); auto retrievedXml = store.getMetadata(DataType::XML, gcid); auto retrievedYaml = store.getMetadata(DataType::YAML, gcid); REQUIRE_FALSE(retrievedXml.empty()); REQUIRE_FALSE(retrievedYaml.empty()); REQUIRE(retrievedXml == xmlData); REQUIRE(retrievedYaml == yamlData); store.close(); } SECTION("Package operations") { DataStore store; store.open(tempDir.string(), mediaDir.string()); const std::string pkgId = "testpkg/1.0.0/amd64"; // Initially, package should not exist REQUIRE_FALSE(store.packageExists(pkgId)); REQUIRE_FALSE(store.isIgnored(pkgId)); // Test package value operations std::string pkgValue = store.getPackageValue(pkgId); REQUIRE(pkgValue.empty()); // Mark package as ignored REQUIRE_NOTHROW(store.setPackageIgnore(pkgId)); REQUIRE(store.packageExists(pkgId)); REQUIRE(store.isIgnored(pkgId)); // Remove package REQUIRE_NOTHROW(store.removePackage(pkgId)); REQUIRE_FALSE(store.packageExists(pkgId)); store.close(); } SECTION("Hints storage and retrieval") { DataStore store; store.open(tempDir.string(), mediaDir.string()); const std::string pkgId = "hintpkg/2.0.0/amd64"; const std::string hintsJson = R"({ "hints": { "sugar-emulator.desktop": [ { "tag": "no-metainfo", "vars": {} }, { "tag": "description-missing", "vars": { "kind": "desktop-application" } } ] }, "package": "sugar-emulator-0.96\/0.96.1-2.1\/all" })"; // Initially, hints should not exist REQUIRE_FALSE(store.hasHints(pkgId)); // Store hints REQUIRE_NOTHROW(store.setHints(pkgId, hintsJson)); // Hints should exist now REQUIRE(store.hasHints(pkgId)); // Retrieve and verify hints std::string retrievedHints = store.getHints(pkgId); REQUIRE_FALSE(retrievedHints.empty()); REQUIRE(retrievedHints == hintsJson); store.close(); } SECTION("Statistics operations") { DataStore store; store.open(tempDir.string(), mediaDir.string()); // Create test statistics data using the new binary format std::unordered_map> statsData = { {"suite", std::string("testing")}, {"section", std::string("main") }, {"totalInfos", 123 }, {"totalWarnings", 24 }, {"totalErrors", 8 }, {"totalMetadata", 42 } }; // Add statistics REQUIRE_NOTHROW(store.addStatistics(statsData)); // Retrieve statistics auto allStats = store.getStatistics(); REQUIRE_FALSE(allStats.empty()); REQUIRE(allStats.size() >= 1); // Verify the data in the first entry const auto &firstEntry = allStats[0]; REQUIRE(firstEntry.data.contains("suite")); REQUIRE(firstEntry.data.contains("section")); REQUIRE(firstEntry.data.contains("totalInfos")); REQUIRE(std::get(firstEntry.data.at("suite")) == "testing"); REQUIRE(std::get(firstEntry.data.at("section")) == "main"); REQUIRE(std::get(firstEntry.data.at("totalInfos")) == 123); REQUIRE(std::get(firstEntry.data.at("totalWarnings")) == 24); REQUIRE(std::get(firstEntry.data.at("totalErrors")) == 8); REQUIRE(std::get(firstEntry.data.at("totalMetadata")) == 42); store.close(); } SECTION("Repository info operations") { DataStore store; store.open(tempDir.string(), mediaDir.string()); const std::string suite = "focal"; const std::string section = "main"; const std::string arch = "amd64"; RepoInfo repoInfo; repoInfo.data["mtime"] = std::int64_t(1753758538); repoInfo.data["last_updated"] = 1643723400.0; // timestamp as double // Set repository info REQUIRE_NOTHROW(store.setRepoInfo(suite, section, arch, repoInfo)); // Retrieve repository info RepoInfo retrievedRepoInfo = store.getRepoInfo(suite, section, arch); REQUIRE_FALSE(retrievedRepoInfo.data.empty()); // Verify the content REQUIRE(retrievedRepoInfo.data.contains("mtime")); REQUIRE(retrievedRepoInfo.data.contains("last_updated")); REQUIRE(std::get(retrievedRepoInfo.data.at("mtime")) == 1753758538); REQUIRE(std::get(retrievedRepoInfo.data.at("last_updated")) == 1643723400.0); // Remove repository info REQUIRE_NOTHROW(store.removeRepoInfo(suite, section, arch)); // Verify removal - should return empty RepoInfo RepoInfo removedRepoInfo = store.getRepoInfo(suite, section, arch); REQUIRE(removedRepoInfo.data.empty()); store.close(); } SECTION("GCID operations") { DataStore store; store.open(tempDir.string(), mediaDir.string()); const std::string pkgId = "gcidpkg/1.0/amd64"; // Initially no GCIDs auto gcids = store.getGCIDsForPackage(pkgId); REQUIRE(gcids.empty()); // Create a dummy package for testing using the existing DummyPackage class auto dummyPkg = std::make_shared("gcidpkg", "1.0", "amd64"); dummyPkg->setMaintainer("Test Maintainer "); dummyPkg->setDescription("A test package for GCID operations", "C"); // Create a GeneratorResult with test components GeneratorResult gres(dummyPkg); // Create test components manually using AppStream g_autoptr(AsComponent) cpt1 = as_component_new(); as_component_set_kind(cpt1, AS_COMPONENT_KIND_DESKTOP_APP); as_component_set_id(cpt1, "org.example.abiword"); as_component_set_name(cpt1, "AbiWord", "C"); as_component_set_summary(cpt1, "Word Processor", "C"); g_autoptr(AsComponent) cpt2 = as_component_new(); as_component_set_kind(cpt2, AS_COMPONENT_KIND_DESKTOP_APP); as_component_set_id(cpt2, "org.kde.ark"); as_component_set_name(cpt2, "Ark", "C"); as_component_set_summary(cpt2, "Archive Manager", "C"); // Add components to the result gres.addComponent(cpt1); gres.addComponent(cpt2); // Verify components were added REQUIRE(gres.componentsCount() == 2); // Test XML metadata generation and storage REQUIRE_NOTHROW(store.addGeneratorResult(DataType::XML, gres, false)); // Now we should have GCIDs for the package auto retrievedGcids = store.getGCIDsForPackage(pkgId); REQUIRE_FALSE(retrievedGcids.empty()); REQUIRE(retrievedGcids.size() == 2); // Get the actual GCIDs that were generated auto actualGcids = gres.getComponentGcids(); REQUIRE(actualGcids.size() == 2); // Verify that the stored GCIDs match what was generated for (const auto &gcid : actualGcids) { REQUIRE(std::find(retrievedGcids.begin(), retrievedGcids.end(), gcid) != retrievedGcids.end()); } // Test that metadata was actually stored for each GCID for (const auto &gcid : actualGcids) { REQUIRE(store.metadataExists(DataType::XML, gcid)); auto metadata = store.getMetadata(DataType::XML, gcid); REQUIRE_FALSE(metadata.empty()); // Verify the metadata contains expected component information if (gcid.find("abiword") != std::string::npos || metadata.find("AbiWord") != std::string::npos) { REQUIRE(metadata.find("org.example.abiword") != std::string::npos); } else if (gcid.find("ark") != std::string::npos || metadata.find("Ark") != std::string::npos) { REQUIRE(metadata.find("org.kde.ark") != std::string::npos); } } // Test YAML metadata generation as well GeneratorResult gresYaml(dummyPkg); gresYaml.addComponent(cpt1); gresYaml.addComponent(cpt2); REQUIRE_NOTHROW(store.addGeneratorResult(DataType::YAML, gresYaml, false)); // Verify YAML metadata was stored for (const auto &gcid : gresYaml.getComponentGcids()) { REQUIRE(store.metadataExists(DataType::YAML, gcid)); auto yamlMetadata = store.getMetadata(DataType::YAML, gcid); REQUIRE_FALSE(yamlMetadata.empty()); } // Test getMetadataForPackage auto xmlMetadataList = store.getMetadataForPackage(DataType::XML, pkgId); REQUIRE(xmlMetadataList.size() == 2); auto yamlMetadataList = store.getMetadataForPackage(DataType::YAML, pkgId); REQUIRE(yamlMetadataList.size() == 2); // Test regeneration behavior (alwaysRegenerate = false) GeneratorResult gresNoRegen(dummyPkg); gresNoRegen.addComponent(cpt1); gresNoRegen.addComponent(cpt2); // This should not regenerate since metadata already exists store.addGeneratorResult(DataType::XML, gresNoRegen, false); // Test forced regeneration (alwaysRegenerate = true) GeneratorResult gresForceRegen(dummyPkg); gresForceRegen.addComponent(cpt1); gresForceRegen.addComponent(cpt2); // This should regenerate even though metadata exists store.addGeneratorResult(DataType::XML, gresForceRegen, true); store.close(); } SECTION("Package matching") { DataStore store; store.open(tempDir.string(), mediaDir.string()); // Add some test packages const std::vector testPackages = { "myapp/1.0/amd64", "myapp/2.0/amd64", "mylib/1.5/amd64", "otherapp/3.0/i386"}; for (const auto &pkgId : testPackages) { store.setPackageIgnore(pkgId); } // Test prefix matching auto matches = store.getPkidsMatching("myapp"); REQUIRE(matches.size() == 2); REQUIRE(std::find(matches.begin(), matches.end(), "myapp/1.0/amd64") != matches.end()); REQUIRE(std::find(matches.begin(), matches.end(), "myapp/2.0/amd64") != matches.end()); auto libMatches = store.getPkidsMatching("mylib"); REQUIRE(libMatches.size() == 1); REQUIRE(libMatches[0] == "mylib/1.5/amd64"); auto noMatches = store.getPkidsMatching("nonexistent"); REQUIRE(noMatches.empty()); store.close(); } SECTION("Package ID sets") { DataStore store; store.open(tempDir.string(), mediaDir.string()); const std::vector testPackages = {"pkg1/1.0/amd64", "pkg2/2.0/amd64", "pkg3/3.0/i386"}; // Add packages for (const auto &pkgId : testPackages) store.setPackageIgnore(pkgId); // Get package ID set auto pkgSet = store.getPackageIdSet(); REQUIRE(pkgSet.size() == testPackages.size()); for (const auto &pkgId : testPackages) REQUIRE(pkgSet.contains(pkgId)); // Test bulk removal std::unordered_set toRemove = {testPackages[0], testPackages[2]}; REQUIRE_NOTHROW(store.removePackages(toRemove)); // Verify removal REQUIRE_FALSE(store.packageExists(testPackages[0])); REQUIRE(store.packageExists(testPackages[1])); // Should still exist REQUIRE_FALSE(store.packageExists(testPackages[2])); store.close(); } // Cleanup fs::remove_all(tempDir); fs::remove_all(mediaDir); } TEST_CASE("DataStore thread safety", "[datastore][threading]") { auto tempDir = fs::temp_directory_path() / std::format("asgen-datastore-test-mt-{}", Utils::randomString(8)); auto mediaDir = fs::temp_directory_path() / std::format("asgen-media-test-mt-{}", Utils::randomString(8)); fs::create_directories(tempDir); fs::create_directories(mediaDir); DataStore store; store.open(tempDir.string(), mediaDir.string()); const int numThreads = 4; const int itemsPerThread = 10; std::vector threads; // Launch multiple threads that store metadata concurrently for (int t = 0; t < numThreads; ++t) { threads.emplace_back([&store, t, itemsPerThread]() { for (int i = 0; i < itemsPerThread; ++i) { std::string gcid = std::format("org.example.thread{}.item{}", t, i); std::string xmlData = std::format( R"( {} Thread {} Item {} )", gcid, t, i); store.setMetadata(DataType::XML, gcid, xmlData); } }); } // Wait for all threads for (auto &thread : threads) thread.join(); // Verify thread safety by checking a few items for (int t = 0; t < 2; ++t) { for (int i = 0; i < 2; ++i) { std::string gcid = std::format("org.example.thread{}.item{}", t, i); REQUIRE(store.metadataExists(DataType::XML, gcid)); } } store.close(); fs::remove_all(tempDir); fs::remove_all(mediaDir); } appstream-generator-0.10.1/tests/tests-engine.cpp000066400000000000000000000042551506754475600220550ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * SPDX-License-Identifier: LGPL-3.0-or-later */ #define CATCH_CONFIG_MAIN #include #include #include #include "logging.h" #include "utils.h" #include "config.h" #include "engine.h" using namespace ASGenerator; static struct TestSetup { TestSetup() { setVerbose(true); } } testSetup; TEST_CASE("Engine with test data", "[engine][integration]") { auto tempDir = fs::temp_directory_path() / std::format("asgen-test-{}", Utils::randomString(8)); fs::create_directories(tempDir); auto samplesDir = Utils::getTestSamplesDir(); SECTION("Test init with Debian backend") { auto debianSamplesDir = samplesDir / "debian"; auto &config = Config::get(); config.setWorkspaceDir("/tmp"); config.backend = Backend::Debian; config.archiveRoot = debianSamplesDir.string(); REQUIRE_NOTHROW([]() { Engine engine; }()); } fs::remove_all(tempDir); } TEST_CASE("Engine package info functionality", "[engine]") { auto tempDir = fs::temp_directory_path() / std::format("asgen-test-{}", Utils::randomString(8)); fs::create_directories(tempDir); auto &config = Config::get(); config.backend = Backend::Dummy; config.setWorkspaceDir("/tmp"); config.archiveRoot = (Utils::getTestSamplesDir() / "debian").string(); Engine engine; SECTION("Print package info with invalid ID format") { // Test with malformed package ID (should return false) REQUIRE_FALSE(engine.printPackageInfo("invalid-package-id")); REQUIRE_FALSE(engine.printPackageInfo("too/many/slashes/here")); REQUIRE_FALSE(engine.printPackageInfo("notEnoughSlashes")); } SECTION("Print package info with valid ID format") { // Test with properly formatted package ID (even if package doesn't exist) // This should return true as the format is correct, even if no data is found REQUIRE(engine.printPackageInfo("package/1.0.0/amd64")); REQUIRE(engine.printPackageInfo("test-pkg/2.1.0/i386")); } fs::remove_all(tempDir); } appstream-generator-0.10.1/tests/tests-icons.cpp000066400000000000000000000070151506754475600217200ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * SPDX-License-Identifier: LGPL-3.0-or-later */ #define CATCH_CONFIG_MAIN #include #include #include #include #include #include #include #include #include #include #include "utils.h" #include "logging.h" #include "iconhandler.h" using namespace ASGenerator; static struct TestSetup { TestSetup() { setVerbose(true); } } testSetup; TEST_CASE("IconHandler", "[IconHandler]") { auto hicolorThemeIndex = Utils::getDataPath("hicolor-theme-index.theme"); // Read theme index data std::vector indexData; std::ifstream f(hicolorThemeIndex, std::ios::binary); REQUIRE(f.is_open()); f.seekg(0, std::ios::end); indexData.resize(f.tellg()); f.seekg(0, std::ios::beg); f.read(reinterpret_cast(indexData.data()), indexData.size()); auto theme = std::make_unique("hicolor", indexData); // Test matching icon filenames for accessories-calculator 48x48 for (const auto &fname : theme->matchingIconFilenames("accessories-calculator", ImageSize(48))) { bool valid = false; if (fname.starts_with("/usr/share/icons/hicolor/48x48/")) valid = true; if (fname.starts_with("/usr/share/icons/hicolor/scalable/")) valid = true; REQUIRE(valid); // Check if icon is allowed format bool formatAllowed = IconHandler::iconAllowed(fname); if (fname.ends_with(".ico")) REQUIRE_FALSE(formatAllowed); else REQUIRE(formatAllowed); } // Test matching icon filenames for accessories-text-editor 192x192 for (const auto &fname : theme->matchingIconFilenames("accessories-text-editor", ImageSize(192))) { bool validPath = false; if (fname.starts_with("/usr/share/icons/hicolor/192x192/")) validPath = true; if (fname.starts_with("/usr/share/icons/hicolor/256x256/")) validPath = true; if (fname.starts_with("/usr/share/icons/hicolor/512x512/")) validPath = true; if (fname.starts_with("/usr/share/icons/hicolor/scalable/")) validPath = true; REQUIRE(validPath); } } TEST_CASE("Theme parsing", "[Theme]") { auto hicolorThemeIndex = Utils::getDataPath("hicolor-theme-index.theme"); REQUIRE(std::filesystem::exists(hicolorThemeIndex)); std::vector indexData; std::ifstream f(hicolorThemeIndex, std::ios::binary); REQUIRE(f.is_open()); f.seekg(0, std::ios::end); indexData.resize(f.tellg()); f.seekg(0, std::ios::beg); f.read(reinterpret_cast(indexData.data()), indexData.size()); auto theme = std::make_unique("hicolor", indexData); REQUIRE(theme->name() == "hicolor"); REQUIRE_FALSE(theme->directories().empty()); // Test that we can find directories that match various sizes bool found16x16Match = false; bool found48x48Match = false; for (const auto &dir : theme->directories()) { // Check if we can find a directory that matches 16x16 (base size) if (theme->directoryMatchesSize(dir, ImageSize(16), false)) { found16x16Match = true; } // Check if we can find a directory that matches 48x48 if (theme->directoryMatchesSize(dir, ImageSize(48), false)) { found48x48Match = true; } } REQUIRE(found16x16Match); REQUIRE(found48x48Match); } appstream-generator-0.10.1/tests/tests-misc.cpp000066400000000000000000000446471506754475600215540ustar00rootroot00000000000000/* * Copyright (C) 2016-2025 Matthias Klumpp * * SPDX-License-Identifier: LGPL-3.0-or-later */ #define CATCH_CONFIG_MAIN #include #include #include #include #include #include #include "logging.h" #include "utils.h" #include "zarchive.h" #include "hintregistry.h" #include "result.h" #include "backends/dummy/dummypkg.h" #include "cptmodifiers.h" using namespace ASGenerator; using namespace ASGenerator::Utils; static struct TestSetup { TestSetup() { // Enable verbose logging for tests setVerbose(true); } } testSetup; TEST_CASE("Compressed empty file decompresses to empty string", "[zarchive]") { // gzip-compressed empty file std::vector emptyGz = { 0x1f, 0x8b, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; REQUIRE(decompressData(emptyGz) == ""); } TEST_CASE("Extracting a tarball", "[zarchive]") { std::string archive = fs::path(Utils::getTestSamplesDir()) / "test.tar.xz"; REQUIRE(fs::exists(archive)); ArchiveDecompressor ar; // Create a temporary directory std::string tmpdir = fs::temp_directory_path() / fs::path("asgenXXXXXX"); std::vector ctmpdir(tmpdir.begin(), tmpdir.end()); ctmpdir.push_back('\0'); char *mkdtemp_result = mkdtemp(ctmpdir.data()); REQUIRE(mkdtemp_result != nullptr); tmpdir = std::string(mkdtemp_result); auto cleanup = [&tmpdir](void *) { fs::remove_all(tmpdir); }; std::unique_ptr guard((void *)1, cleanup); ar.open(archive); ar.extractArchive(tmpdir); std::string path = fs::path(tmpdir) / "b" / "a"; REQUIRE(fs::exists(path)); std::ifstream f(path); REQUIRE(f); std::string content; std::getline(f, content); // Remove trailing newline if present if (!content.empty() && content.back() == '\n') content.pop_back(); REQUIRE(content == "hello"); // Read regular file which has a hardlink pointing to it std::string test_path = fs::path(tmpdir) / "test.txt"; REQUIRE(fs::exists(test_path)); std::ifstream f2(test_path); REQUIRE(f2); std::string test_content; std::getline(f2, test_content); // Remove trailing newline if present if (!test_content.empty() && test_content.back() == '\n') test_content.pop_back(); REQUIRE(test_content == "Wow!"); // Verify the hardlink contents matches the original file std::string hardlink_path = fs::path(tmpdir) / "e" / "f"; REQUIRE(fs::exists(hardlink_path)); std::ifstream f3(hardlink_path); REQUIRE(f3); std::string hardlink_content; std::getline(f3, hardlink_content); // Remove trailing newline if present if (!hardlink_content.empty() && hardlink_content.back() == '\n') hardlink_content.pop_back(); REQUIRE(test_content == hardlink_content); } TEST_CASE("Reading data from tarball using readData", "[zarchive]") { std::string archive = fs::path(Utils::getTestSamplesDir()) / "test.tar.xz"; REQUIRE(fs::exists(archive)); ArchiveDecompressor ar; ar.open(archive); SECTION("Read specific files directly from archive") { // Test reading a known file from the test archive auto data = ar.readData("b/a"); REQUIRE(!data.empty()); std::string content(data.begin(), data.end()); // Remove trailing newline if present if (!content.empty() && content.back() == '\n') content.pop_back(); REQUIRE(content == "hello"); // Test reading another file auto data2 = ar.readData("c/d"); REQUIRE(!data2.empty()); std::string content2(data2.begin(), data2.end()); // Remove trailing newline if present if (!content2.empty() && content2.back() == '\n') content2.pop_back(); REQUIRE(content2 == "world"); // Read with starting slash auto data3 = ar.readData("/c/d"); REQUIRE(!data3.empty()); // Ensure we follow hardlinks auto data4 = ar.readData("e/f"); REQUIRE(!data4.empty()); } SECTION("Read with path variations") { // Test that paths with leading slash work the same auto data1 = ar.readData("b/a"); auto data2 = ar.readData("/b/a"); REQUIRE(data1 == data2); } SECTION("Read non-existent file throws exception") { REQUIRE_THROWS_AS(ar.readData("non/existent/file"), std::runtime_error); } ar.close(); } TEST_CASE("Utils: getCidFromGlobalID", "[utils]") { REQUIRE(getCidFromGlobalID("f/fo/foobar.desktop/DEADBEEF").value() == "foobar.desktop"); REQUIRE(getCidFromGlobalID("org/gnome/yelp.desktop/DEADBEEF").value() == "org.gnome.yelp.desktop"); REQUIRE_FALSE(getCidFromGlobalID("invalid/only/three").has_value()); REQUIRE_FALSE(getCidFromGlobalID("").has_value()); } TEST_CASE("Utils: localeValid returns false for x-test and xx, true otherwise", "[utils]") { REQUIRE_FALSE(localeValid("x-test")); REQUIRE_FALSE(localeValid("xx")); REQUIRE(localeValid("en_US")); REQUIRE(localeValid("de")); } TEST_CASE("Utils: getTextFileContents and getFileContents read file data", "[utils]") { auto tmpfile = fs::temp_directory_path() / "asgen_testfile.txt"; { std::ofstream f(tmpfile); f << "line1\nline2\n"; } auto lines = getTextFileContents(tmpfile.string()); REQUIRE(lines.size() == 2); REQUIRE(lines[0] == "line1"); REQUIRE(lines[1] == "line2"); auto bytes = getFileContents(tmpfile.string()); REQUIRE(bytes.size() == 12); // 6+6 including newlines fs::remove(tmpfile); } TEST_CASE("Selectively reading tarball", "[zarchive]") { std::string archive = fs::path(getTestSamplesDir()) / "test.tar.xz"; REQUIRE(fs::exists(archive)); ArchiveDecompressor ar; ar.open(archive); SECTION("Full iteration through all entries") { std::vector filenames; std::vector> fileData; for (const auto &entry : ar.read()) { filenames.push_back(entry.fname); fileData.push_back(entry.data); } // Should have found files from the test archive REQUIRE(!filenames.empty()); // Check that we got the expected file auto it = std::find(filenames.begin(), filenames.end(), "/b/a"); REQUIRE(it != filenames.end()); // Get the data for file "/b/a" and verify content size_t index = std::distance(filenames.begin(), it); std::string content(fileData[index].begin(), fileData[index].end()); // Remove trailing newline if present if (!content.empty() && content.back() == '\n') content.pop_back(); REQUIRE(content == "hello"); } SECTION("Early termination when finding specific file") { int entriesProcessed = 0; bool foundTargetFile = false; std::string targetContent; for (const auto &entry : ar.read()) { entriesProcessed++; if (entry.fname == "/c/d") { foundTargetFile = true; targetContent = std::string(entry.data.begin(), entry.data.end()); // Remove trailing newline if present if (!targetContent.empty() && targetContent.back() == '\n') targetContent.pop_back(); break; // Early termination } } REQUIRE(foundTargetFile); REQUIRE(targetContent == "world"); // Should have processed fewer entries than total (early termination worked) REQUIRE(entriesProcessed > 0); REQUIRE(entriesProcessed <= 10); // Reasonable upper bound for test archive } SECTION("Multiple iterations over same archive") { // Test that we can iterate multiple times int firstCount = 0; for (const auto &entry : ar.read()) { firstCount++; (void)entry; // Suppress unused variable warning } int secondCount = 0; for (const auto &entry : ar.read()) { secondCount++; (void)entry; } // Both iterations should yield the same number of entries REQUIRE(firstCount > 0); REQUIRE(firstCount == secondCount); } ar.close(); } TEST_CASE("Image size operations", "[utils][imagesize]") { SECTION("ImageSize construction and comparison") { ImageSize size1(64); ImageSize size2(64, 64, 1); ImageSize size3(64, 64, 2); // HiDPI ImageSize size4(128); REQUIRE(size1 == size2); REQUIRE(size1 != size3); REQUIRE(size1 != size4); REQUIRE(size3 != size4); // Test scale differences REQUIRE(size1.scale == 1); REQUIRE(size3.scale == 2); } SECTION("ImageSize string representation") { ImageSize size1(64); ImageSize size2(128, 128, 2); REQUIRE(size1.toString() == "64x64"); REQUIRE(size2.toString() == "128x128@2"); ImageSize size3("64x64"); REQUIRE(size3.width == 64); REQUIRE(size3.height == 64); REQUIRE(size3.scale == 1); ImageSize size4("128x128@2"); REQUIRE(size4.width == 128); REQUIRE(size4.height == 128); REQUIRE(size4.scale == 2); } SECTION("ImageSize ordering") { ImageSize small(48); ImageSize medium(64); ImageSize large(128); ImageSize mediumHiDPI(64, 64, 2); ImageSize largeHiDPI(128, 128, 2); REQUIRE(small < medium); REQUIRE(medium < large); REQUIRE(medium < mediumHiDPI); // Same size but higher scale REQUIRE(medium < largeHiDPI); REQUIRE(medium == ImageSize(64, 64, 1)); REQUIRE_FALSE(medium == largeHiDPI); } } TEST_CASE("HintRegistry functionality", "[hintregistry]") { using namespace ASGenerator; SECTION("Load hints registry") { g_autoptr(AscHint) hint = nullptr; g_autoptr(GError) error = nullptr; // tag must not exist at this point hint = asc_hint_new_for_tag("description-from-package", &error); REQUIRE(error != nullptr); REQUIRE(hint == nullptr); g_error_free(g_steal_pointer(&error)); // Test loading the hints registry REQUIRE_NOTHROW(loadHintsRegistry()); // after loading the registry, the tag should exist hint = asc_hint_new_for_tag("description-from-package", &error); if (error != nullptr) FAIL(std::format("Error creating hint: {}", error->message)); REQUIRE(hint != nullptr); // Test that some common hint tags are loaded REQUIRE(asc_globals_hint_tag_severity("icon-not-found") != AS_ISSUE_SEVERITY_UNKNOWN); REQUIRE(asc_globals_hint_tag_severity("no-metainfo") != AS_ISSUE_SEVERITY_UNKNOWN); REQUIRE(asc_globals_hint_tag_severity("internal-error") != AS_ISSUE_SEVERITY_UNKNOWN); // Test severity retrieval auto severity = asc_globals_hint_tag_severity("icon-not-found"); REQUIRE(severity != AS_ISSUE_SEVERITY_UNKNOWN); // Test explanation retrieval std::string explanation = asc_globals_hint_tag_explanation("icon-not-found"); REQUIRE_FALSE(explanation.empty()); } SECTION("Retrieve hint definition") { auto hdef = retrieveHintDef("icon-not-found"); REQUIRE(hdef.tag == "icon-not-found"); REQUIRE(hdef.severity == AS_ISSUE_SEVERITY_ERROR); REQUIRE_FALSE(hdef.explanation.empty()); // Test non-existent hint auto emptyHdef = retrieveHintDef("non-existent-hint"); REQUIRE(emptyHdef.tag.empty()); REQUIRE(emptyHdef.severity == AS_ISSUE_SEVERITY_UNKNOWN); REQUIRE(emptyHdef.explanation.empty()); } SECTION("Hint to JSON conversion") { std::unordered_map vars = { {"test_key", "test_value" }, {"another_key", "another_value"} }; auto jsonStr = hintToJsonString("test-tag", vars); REQUIRE_FALSE(jsonStr.empty()); REQUIRE(jsonStr != "{}"); // Basic JSON validation - should contain our data REQUIRE(jsonStr.find("test-tag") != std::string::npos); REQUIRE(jsonStr.find("test_key") != std::string::npos); REQUIRE(jsonStr.find("test_value") != std::string::npos); } SECTION("Save hints registry to JSON file") { auto tempFile = fs::temp_directory_path() / "test-hints-registry.json"; REQUIRE_NOTHROW(saveHintsRegistryToJsonFile(tempFile.string())); REQUIRE(fs::exists(tempFile)); // Verify file has content auto fileSize = fs::file_size(tempFile); REQUIRE(fileSize > 0); // Clean up fs::remove(tempFile); } } TEST_CASE("GeneratorResult functionality", "[result]") { using namespace ASGenerator; auto pkg = std::make_shared("foobar", "1.0.0", "amd64"); SECTION("Basic GeneratorResult operations") { GeneratorResult result(pkg); // Test package ID REQUIRE(result.pkid() == "foobar/1.0.0/amd64"); // Test package retrieval REQUIRE(result.getPackage() == pkg); REQUIRE(result.getResult() != nullptr); } SECTION("Add hints to result") { GeneratorResult result(pkg); // Ensure hints registry is loaded loadHintsRegistry(); // Add a hint with component ID std::unordered_map vars = { {"icon_fname", "test.png" }, {"additional_info", "test data"} }; bool stillValid = result.addHint("org.test.Component", "icon-not-found", vars); REQUIRE(stillValid == false); // Add a hint with simple message stillValid = result.addHint("org.test.Component2", "no-metainfo", "Test message"); REQUIRE(stillValid); // Verify hints were added REQUIRE(result.hintsCount() > 0); REQUIRE(result.hasHint("org.test.Component", "icon-not-found")); REQUIRE(result.hasHint("org.test.Component2", "no-metainfo")); } SECTION("Generate hints JSON") { GeneratorResult result(pkg); loadHintsRegistry(); // Add some hints const std::unordered_map &vars = { {"rainbows", "yes" }, {"unicorns", "no" }, {"storage", "towel"} }; result.addHint("org.freedesktop.foobar.desktop", "desktop-entry-hidden-set", vars); result.addHint( "org.freedesktop.awesome-bar.desktop", "metainfo-validation-error", "Nothing is good without chocolate. Add some."); result.addHint( "org.freedesktop.awesome-bar.desktop", "screenshot-video-check-failed", "Frobnicate functionality is missing."); // Generate JSON auto jsonStr = result.hintsToJson(); REQUIRE_FALSE(jsonStr.empty()); // Basic validation INFO(jsonStr); REQUIRE(jsonStr.find("foobar/1.0.0/amd64") != std::string::npos); REQUIRE(jsonStr.find("org.freedesktop.awesome-bar.desktop") != std::string::npos); REQUIRE(jsonStr.find("screenshot-video-check-failed") != std::string::npos); REQUIRE(jsonStr.find("desktop-entry-hidden-set") != std::string::npos); } SECTION("Move semantics") { GeneratorResult result1(pkg); loadHintsRegistry(); result1.addHint("test.component", "icon-not-found"); // Test move constructor GeneratorResult result2 = std::move(result1); REQUIRE(result2.pkid() == "foobar/1.0.0/amd64"); REQUIRE(result2.hintsCount() > 0); // Test move assignment GeneratorResult result3(pkg); result3 = std::move(result2); REQUIRE(result3.pkid() == "foobar/1.0.0/amd64"); REQUIRE(result3.hintsCount() > 0); } } TEST_CASE("InjectedModifications", "[cptmodifiers]") { auto dummySuite = std::make_shared(); dummySuite->name = "dummy"; dummySuite->extraMetainfoDir = getTestSamplesDir() / "extra-metainfo"; auto injMods = std::make_unique(); injMods->loadForSuite(std::move(dummySuite)); REQUIRE(injMods->isComponentRemoved("com.example.removed")); REQUIRE_FALSE(injMods->isComponentRemoved("com.example.not_removed")); REQUIRE_FALSE(injMods->injectedCustomData("org.example.nodata").has_value()); auto customData = injMods->injectedCustomData("org.example.newdata"); REQUIRE(customData.has_value()); REQUIRE(customData->at("earth") == "moon"); REQUIRE(customData->at("mars") == "phobos"); REQUIRE(customData->at("saturn") == "thrym"); } TEST_CASE("Utils: UTF-8 sanitization", "[utils]") { SECTION("Remove invalid characters") { std::string input = "Zipper est un outil\x14 pour extraire"; std::string sanitized = Utils::sanitizeUtf8(input); REQUIRE(sanitized == "Zipper est un outil pour extraire"); REQUIRE(sanitized.length() == input.length() - 1); // One character removed } SECTION("Preserve valid UTF-8 characters") { std::string input = "Café résumé naïve"; std::string sanitized = Utils::sanitizeUtf8(input); REQUIRE(sanitized == input); // Should be unchanged } SECTION("Preserve valid control characters") { std::string input = "Valid text with tab\t, newline\n, and carriage return\r."; std::string sanitized = Utils::sanitizeUtf8(input); REQUIRE(sanitized == input); // Should be unchanged } SECTION("Remove multiple invalid control characters") { std::string input = "Text\x01with\x14invalid\x1F" "characters"; std::string sanitized = Utils::sanitizeUtf8(input); REQUIRE(sanitized == "Textwithinvalidcharacters"); } SECTION("Handle invalid UTF-8 sequences") { std::string input = "Valid text \xFF\xFE invalid UTF-8"; std::string sanitized = Utils::sanitizeUtf8(input); REQUIRE(sanitized == "Valid text invalid UTF-8"); // Should be shorter due to removed invalid bytes REQUIRE(sanitized.length() < input.length()); } SECTION("Preserve 4-byte UTF-8 emoji") { std::string input = "Hello 🌍 World! 😀"; std::string sanitized = Utils::sanitizeUtf8(input); REQUIRE(sanitized == input); // Should preserve emoji characters } } appstream-generator-0.10.1/tests/tests-net.cpp000066400000000000000000000163471506754475600214030ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * SPDX-License-Identifier: LGPL-3.0-or-later */ #define CATCH_CONFIG_MAIN #include #include #include #include #include #include "downloader.h" #include "utils.h" #include "logging.h" using namespace ASGenerator; static struct TestSetup { TestSetup() { setVerbose(true); } } testSetup; static bool canRunNetworkTests() { // Check if network tests should be skipped const char *skipNetEnv = std::getenv("ASGEN_TESTS_NO_NET"); if (skipNetEnv && std::string(skipNetEnv) != "no") { SKIP("Network dependent tests skipped (explicitly disabled via ASGEN_TESTS_NO_NET)"); return false; } auto &downloader = Downloader::get(); const std::string urlFirefoxDetectportal = "https://detectportal.firefox.com/"; try { downloader.downloadText(urlFirefoxDetectportal); } catch (const DownloadException &e) { SKIP("Network dependent tests skipped (automatically, no network detected: " + std::string(e.what()) + ")"); return false; } return true; } TEST_CASE("Downloader functionality", "[downloader][network]") { if (!canRunNetworkTests()) return; auto &downloader = Downloader::get(); const std::string urlFirefoxDetectportal = "https://detectportal.firefox.com/"; SECTION("File download functionality") { const std::string testFileName = "/tmp/asgen-test-ffdp-" + Utils::randomString(4); // Clean up file on exit auto cleanup = [&testFileName]() { if (std::filesystem::exists(testFileName)) { std::filesystem::remove(testFileName); } }; try { downloader.downloadFile(urlFirefoxDetectportal, testFileName); // Verify file contents std::ifstream file(testFileName); REQUIRE(file.is_open()); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); REQUIRE(content == "success\n"); cleanup(); } catch (const DownloadException &e) { cleanup(); SKIP("Network test skipped: " + std::string(e.what())); } } SECTION("Download larger file") { const std::string testFileName = "/tmp/asgen-test-debian-" + Utils::randomString(4); auto cleanup = [&testFileName]() { if (std::filesystem::exists(testFileName)) { std::filesystem::remove(testFileName); } }; try { downloader.downloadFile("https://debian.org", testFileName); // Verify file exists and has content REQUIRE(std::filesystem::exists(testFileName)); REQUIRE(std::filesystem::file_size(testFileName) > 0); cleanup(); } catch (const DownloadException &e) { cleanup(); SKIP("Network test skipped: " + std::string(e.what())); } } SECTION("Error handling for non-existent file") { const std::string testFileName = "/tmp/asgen-dltest-" + Utils::randomString(4); auto cleanup = [&testFileName]() { if (std::filesystem::exists(testFileName)) { std::filesystem::remove(testFileName); } }; try { REQUIRE_THROWS_AS( downloader.downloadFile("https://appstream.debian.org/nonexistent", testFileName, 2), DownloadException); cleanup(); } catch (...) { cleanup(); throw; } } SECTION("HTTP to HTTPS redirect handling") { const std::string testFileName = "/tmp/asgen-test-mozilla-" + Utils::randomString(4); auto cleanup = [&testFileName]() { if (std::filesystem::exists(testFileName)) { std::filesystem::remove(testFileName); } }; try { // This should work as mozilla.org redirects HTTP to HTTPS downloader.downloadFile("http://mozilla.org", testFileName, 1); // Verify file exists and has content REQUIRE(std::filesystem::exists(testFileName)); REQUIRE(std::filesystem::file_size(testFileName) > 0); cleanup(); } catch (const DownloadException &e) { cleanup(); SKIP("Network test skipped: " + std::string(e.what())); } } SECTION("Download to memory") { try { auto data = downloader.download(urlFirefoxDetectportal); std::string content(data.begin(), data.end()); REQUIRE(content == "success\n"); } catch (const DownloadException &e) { SKIP("Network test skipped: " + std::string(e.what())); } } SECTION("Download text lines") { try { auto lines = downloader.downloadTextLines(urlFirefoxDetectportal); REQUIRE(lines.size() == 1); REQUIRE(lines[0] == "success"); } catch (const DownloadException &e) { SKIP("Network test skipped: " + std::string(e.what())); } } } TEST_CASE("Downloader edge cases", "[downloader]") { if (!canRunNetworkTests()) return; auto &downloader = Downloader::get(); SECTION("Invalid URL handling") { REQUIRE_THROWS_AS(downloader.downloadText("not-a-url"), DownloadException); } SECTION("Empty URL handling") { REQUIRE_THROWS_AS(downloader.downloadText(""), DownloadException); } SECTION("Retry mechanism with zero retries") { REQUIRE_THROWS_AS(downloader.downloadText("https://nonexistent.example.invalid", 0), DownloadException); } } TEST_CASE("Downloader file skipping", "[downloader]") { if (!canRunNetworkTests()) return; auto &downloader = Downloader::get(); SECTION("Skip download if file already exists") { const std::string testFileName = "/tmp/asgen-test-existing-" + Utils::randomString(4); // Create a file first { std::ofstream file(testFileName); file << "existing content\n"; } auto cleanup = [&testFileName]() { if (std::filesystem::exists(testFileName)) { std::filesystem::remove(testFileName); } }; try { // This should skip the download since file exists downloader.downloadFile("https://detectportal.firefox.com/", testFileName); // Verify the original content is still there std::ifstream file(testFileName); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); REQUIRE(content == "existing content\n"); cleanup(); } catch (const DownloadException &e) { cleanup(); // If network is not available, the test should still pass // since the file exists and download should be skipped std::ifstream file(testFileName); if (file.is_open()) { std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); REQUIRE(content == "existing content\n"); } } } } appstream-generator-0.10.1/tests/tests-report.cpp000066400000000000000000000357051506754475600221270ustar00rootroot00000000000000/* * Copyright (C) 2019-2025 Matthias Klumpp * * SPDX-License-Identifier: LGPL-3.0-or-later */ #define CATCH_CONFIG_MAIN #include #include #include #include #include #include #include #include #include #include #include "logging.h" #include "reportgenerator.h" #include "datastore.h" #include "config.h" #include "backends/interfaces.h" #include "backends/dummy/dummypkg.h" #include "result.h" #include "hintregistry.h" using namespace ASGenerator; static struct TestSetup { TestSetup() { setVerbose(true); } } testSetup; // Test fixture for report generator tests class ReportGeneratorTestFixture { public: ReportGeneratorTestFixture() { // Create temporary directories for testing m_tempDir = fs::temp_directory_path() / "asgen_test" / std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); fs::create_directories(m_tempDir); m_dbDir = m_tempDir / "db"; m_htmlDir = m_tempDir / "html"; m_mediaDir = m_tempDir / "media"; fs::create_directories(m_dbDir); fs::create_directories(m_htmlDir); fs::create_directories(m_mediaDir); // Use the default templates m_templateDir = Utils::getDataPath("templates/default"); // Create a test configuration file and load configuration auto configFile = createTestConfig(); Config::get().loadFromFile(configFile.string(), m_tempDir.string(), (m_tempDir / "data").string()); // Initialize datastore m_dstore = std::make_unique(); m_dstore->open(m_dbDir.string(), m_mediaDir.string()); // Load the hints registry to avoid hint tag errors loadHintsRegistry(); // Create report generator m_reportGen = std::make_unique(m_dstore.get()); } ~ReportGeneratorTestFixture() { m_reportGen.reset(); m_dstore.reset(); // Clean up temporary directory std::error_code ec; fs::remove_all(m_tempDir, ec); } protected: std::vector> createTestPackages() { std::vector> packages; auto pkg1 = std::make_shared("testpkg1", "1.0.0", "amd64"); pkg1->setMaintainer("Test Maintainer "); pkg1->setFilename("testpkg1_1.0.0_amd64.deb"); packages.push_back(std::move(pkg1)); auto pkg2 = std::make_shared("testpkg2", "2.0.0", "amd64"); pkg2->setMaintainer("Another Maintainer "); pkg2->setFilename("testpkg2_2.0.0_amd64.deb"); packages.push_back(std::move(pkg2)); auto pkg3 = std::make_shared("testpkg3", "1.5.0", "riscv64"); pkg3->setMaintainer("Test Maintainer "); pkg3->setFilename("testpkg3_1.5.0_i386.deb"); packages.push_back(std::move(pkg3)); return packages; } void addTestData() { // Add some test metadata to the datastore m_dstore->setMetadata(DataType::YAML, "test.gcid.1", R"( Type: desktop-application ID: test.app.1 Name: C: Test Application 1 Summary: C: A test application )"); // Add some test hints for testpkg1 m_dstore->setHints("testpkg1/1.0.0/amd64", R"({ "hints": { "test.app.1": [ { "tag": "missing-desktop-file", "vars": { "filename": "test.desktop" } } ] } })"); // Add test hints for testpkg2 so it gets processed by preprocessInformation m_dstore->setHints("testpkg2/2.0.0/amd64", R"({ "hints": { "test.app.2": [ { "tag": "icon-not-found", "vars": { "icon_fname": "test-icon.png" } } ] } })"); } fs::path createTestConfig() { // Create a minimal test configuration auto configFile = m_tempDir / "test-config.json"; std::ofstream configStream(configFile); configStream << R"({ "ProjectName": "Test Project", "ArchiveRoot": "/tmp/archive", "WorkspaceDir": ")" << m_tempDir.string() << R"(", "MediaBaseUrl": "https://example.com/media", "HtmlBaseUrl": "https://example.com/html", "TemplateDir": ")" << m_templateDir.string() << R"(", "ExportDirs": { "Html": ")" << m_htmlDir.string() << R"(", "Media": ")" << m_mediaDir.string() << R"(" }, "Backend": "dummy", "Suites": { "testsuite": { "sections": ["main"], "architectures": ["amd64", "i386"] } } })"; configStream.close(); return configFile; } protected: fs::path m_tempDir; fs::path m_dbDir; fs::path m_htmlDir; fs::path m_mediaDir; fs::path m_templateDir; std::unique_ptr m_dstore; std::unique_ptr m_reportGen; }; TEST_CASE_METHOD(ReportGeneratorTestFixture, "ReportGenerator::preprocessInformation") { addTestData(); auto packages = createTestPackages(); SECTION("Data preprocessing") { auto dsum = m_reportGen->preprocessInformation("testsuite", "main", packages); REQUIRE(!dsum.pkgSummaries.empty()); REQUIRE(!dsum.hintEntries.empty()); // Check that we have the expected maintainer REQUIRE(dsum.pkgSummaries.count("Test Maintainer ") > 0); REQUIRE(dsum.pkgSummaries.count("Another Maintainer ") > 0); // Check hints are processed REQUIRE(dsum.hintEntries.count("testpkg1") > 0); REQUIRE(dsum.hintEntries.count("testpkg2") > 0); } } TEST_CASE_METHOD(ReportGeneratorTestFixture, "ReportGenerator::renderPage") { SECTION("Basic page rendering") { inja::json context; context["suite"] = "testsuite"; context["section"] = "main"; // Add the suites array that the main template expects inja::json suites = inja::json::array(); inja::json suite; suite["suite"] = "testsuite"; suites.push_back(suite); context["suites"] = suites; // Add empty oldsuites array context["oldsuites"] = inja::json::array(); REQUIRE_NOTHROW(m_reportGen->renderPage("main", "test_main", context)); // Check that the file was created auto outputFile = m_htmlDir / "test_main.html"; REQUIRE(fs::exists(outputFile)); // Read and verify basic content std::ifstream file(outputFile); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); REQUIRE(content.find("Test Project") != std::string::npos); } SECTION("Page rendering with complex context") { inja::json context; context["suite"] = "testsuite"; context["section"] = "main"; context["package_name"] = "testpkg"; inja::json entries = inja::json::array(); inja::json entry; entry["component_id"] = "test.app.1"; entry["has_errors"] = true; entry["has_warnings"] = false; entry["has_infos"] = false; // Add the architectures field that the template expects inja::json architectures = inja::json::array(); inja::json arch; arch["arch"] = "amd64"; architectures.push_back(arch); entry["architectures"] = architectures; inja::json errors = inja::json::array(); inja::json error; error["error_tag"] = "test-error"; error["error_description"] = "Test error description"; errors.push_back(error); entry["errors"] = errors; entries.push_back(entry); context["entries"] = entries; REQUIRE_NOTHROW(m_reportGen->renderPage("issues_page", "test_issues", context)); auto outputFile = m_htmlDir / "test_issues.html"; REQUIRE(fs::exists(outputFile)); } } TEST_CASE_METHOD(ReportGeneratorTestFixture, "ReportGenerator Statistics") { SECTION("Export statistics") { // Add some test statistics first std::unordered_map> statsData = { {"suite", std::string("testsuite")}, {"section", std::string("main") }, {"totalInfos", std::int64_t(5) }, {"totalWarnings", std::int64_t(3) }, {"totalErrors", std::int64_t(1) }, {"totalMetadata", std::int64_t(10) } }; m_dstore->addStatistics(statsData); REQUIRE_NOTHROW(m_reportGen->exportStatistics()); auto statsFile = m_htmlDir / "statistics.json"; REQUIRE(fs::exists(statsFile)); std::ifstream file(statsFile); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); REQUIRE(content.find("testsuite") != std::string::npos); REQUIRE(content.find("main") != std::string::npos); REQUIRE(content.find("errors") != std::string::npos); REQUIRE(content.find("warnings") != std::string::npos); REQUIRE(content.find("infos") != std::string::npos); REQUIRE(content.find("metadata") != std::string::npos); // Verify the actual numeric values are present REQUIRE(content.find(",1]") != std::string::npos); // totalErrors: 1 REQUIRE(content.find(",3]") != std::string::npos); // totalWarnings: 3 REQUIRE(content.find(",5]") != std::string::npos); // totalInfos: 5 REQUIRE(content.find(",10]") != std::string::npos); // totalMetadata: 10 } } TEST_CASE_METHOD(ReportGeneratorTestFixture, "ReportGenerator render pages with mock data") { SECTION("Process packages for suite/section") { auto packages = createTestPackages(); REQUIRE_NOTHROW(m_reportGen->processFor("testsuite", "main", packages)); // Check that the section directory structure was created auto sectionDir = m_htmlDir / "testsuite" / "main"; REQUIRE(fs::exists(sectionDir)); } SECTION("Render pages with hint entries") { ReportGenerator::DataSummary dsum; // Create mock hint entry ReportGenerator::HintEntry hentry; hentry.identifier = "test.component.1"; hentry.archs = {"amd64", "i386"}; hentry.errors = { {"error-tag", "Error message"} }; hentry.warnings = { {"warning-tag", "Warning message"} }; hentry.infos = { {"info-tag", "Info message"} }; dsum.hintEntries["testpkg1"]["test.component.1"] = std::move(hentry); // Create mock package summary ReportGenerator::PkgSummary summary; summary.pkgname = "testpkg1"; summary.errorCount = 1; summary.warningCount = 1; summary.infoCount = 1; dsum.pkgSummaries["Test Maintainer"]["testpkg1"] = std::move(summary); REQUIRE_NOTHROW(m_reportGen->renderPagesFor("testsuite", "main", dsum)); // Check that issue pages were created auto issuesIndex = m_htmlDir / "testsuite" / "main" / "issues" / "index.html"; REQUIRE(fs::exists(issuesIndex)); auto issuesPage = m_htmlDir / "testsuite" / "main" / "issues" / "testpkg1.html"; REQUIRE(fs::exists(issuesPage)); // Verify content std::ifstream file(issuesPage); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); REQUIRE(content.find("test.component.1") != std::string::npos); REQUIRE(content.find("Error message") != std::string::npos); REQUIRE(content.find("Warning message") != std::string::npos); REQUIRE(content.find("Info message") != std::string::npos); } SECTION("Render pages with metadata entries") { ReportGenerator::DataSummary dsum; // Create mock metadata entry ReportGenerator::MetadataEntry mentry; mentry.kind = AS_COMPONENT_KIND_DESKTOP_APP; mentry.identifier = "test.app.1"; mentry.archs = {"amd64"}; mentry.data = "Type: desktop-application\nID: test.app.1\n"; mentry.iconName = "test-icon.png"; dsum.mdataEntries["testpkg1"]["1.0.0"]["test.gcid.1"] = std::move(mentry); // Create mock package summary with components ReportGenerator::PkgSummary summary; summary.pkgname = "testpkg1"; summary.cpts = {"test.app.1 - 1.0.0"}; dsum.pkgSummaries["Test Maintainer"]["testpkg1"] = std::move(summary); REQUIRE_NOTHROW(m_reportGen->renderPagesFor("testsuite", "main", dsum)); // Check that metainfo pages were created auto metainfoIndex = m_htmlDir / "testsuite" / "main" / "metainfo" / "index.html"; REQUIRE(fs::exists(metainfoIndex)); auto metainfoPage = m_htmlDir / "testsuite" / "main" / "metainfo" / "testpkg1.html"; REQUIRE(fs::exists(metainfoPage)); // Verify content std::ifstream file(metainfoPage); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); REQUIRE(content.find("test.app.1 - 1.0.0") != std::string::npos); REQUIRE(content.find("Type: desktop-application") != std::string::npos); } SECTION("Render section index page") { ReportGenerator::DataSummary dsum; dsum.totalMetadata = 10; dsum.totalInfos = 5; dsum.totalWarnings = 3; dsum.totalErrors = 1; REQUIRE_NOTHROW(m_reportGen->renderPagesFor("testsuite", "main", dsum)); auto sectionIndex = m_htmlDir / "testsuite" / "main" / "index.html"; REQUIRE(fs::exists(sectionIndex)); // Verify statistics are rendered std::ifstream file(sectionIndex); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); // Check for text that's actually in the section_page.html template REQUIRE( content.find("valid components") != std::string::npos); // From the template: "{{metainfo_count}} valid components" REQUIRE(content.find("errors") != std::string::npos); // From the template REQUIRE(content.find("warnings") != std::string::npos); // From the template } SECTION("Update index pages") { REQUIRE_NOTHROW(m_reportGen->updateIndexPages()); // Check that main index was created auto mainIndex = m_htmlDir / "index.html"; REQUIRE(fs::exists(mainIndex)); // Verify content contains expected elements std::ifstream file(mainIndex); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); // Check for text that's actually in the templates REQUIRE(content.find("Generated by") != std::string::npos); // From base.html footer REQUIRE(content.find("appstream-generator") != std::string::npos); // From base.html footer } }